diff --git a/.devpilot-scripts/.gitignore b/.devpilot-scripts/.gitignore new file mode 100644 index 0000000..2ed1bd0 --- /dev/null +++ b/.devpilot-scripts/.gitignore @@ -0,0 +1,2 @@ +.devpilot-logs +.devpilot-backups \ No newline at end of file diff --git a/.devpilot-scripts/README.md b/.devpilot-scripts/README.md new file mode 100644 index 0000000..de99c80 --- /dev/null +++ b/.devpilot-scripts/README.md @@ -0,0 +1,14 @@ +# e-commerce-microservices-sample DevPilot Scripts + +This directory contains automation scripts managed by DevPilot. + +## Available Commands + +- `devpilot setup-env`: Setup the development environment +- `devpilot start-env`: Start the development environment +- `devpilot stop-env`: Stop the development environment +- `devpilot delete-env`: Delete the development environment +- `devpilot run-server`: Run the development server +- `devpilot build-server`: Build the server +- `devpilot deploy-server`: Deploy the server +- `devpilot test-server`: Run tests diff --git a/.devpilot-scripts/build-and-push-images.ps1 b/.devpilot-scripts/build-and-push-images.ps1 new file mode 100644 index 0000000..bb9f74b --- /dev/null +++ b/.devpilot-scripts/build-and-push-images.ps1 @@ -0,0 +1,3 @@ +# PowerShell script to build and push images +Write-Host "Building and pushing images..." +# Add your image build and push commands here diff --git a/.devpilot-scripts/build-and-push-images.sh b/.devpilot-scripts/build-and-push-images.sh new file mode 100755 index 0000000..c199dbd --- /dev/null +++ b/.devpilot-scripts/build-and-push-images.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# Helper Functions +function write_phase() { + echo -e "\nπŸš€ $1" +} + +function write_step() { + echo -e "\nπŸ“‹ $1" +} + +function write_progress() { + echo -e " $1" +} + +function write_success() { + echo -e "βœ… $1" +} + +function write_warning() { + echo -e "⚠️ $1" +} + +function write_error() { + echo -e "❌ $1" +} + +# Configuration - Can be overridden by environment variables +registry_url=${REGISTRY_URL:-"image.registry.local:5001"} + +# Get the script directory and project directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Load service locations from devpilot.json +DEVPILOT_CONFIG="$PROJECT_DIR/.devpilot.json" +if [ ! -f "$DEVPILOT_CONFIG" ]; then + write_error "DevPilot configuration file not found at $DEVPILOT_CONFIG" + write_error "Please run 'devpilot init' first to configure your project" + exit 1 +fi + +write_phase "Loading service configuration from DevPilot config" +# Use jq if available, otherwise use grep and sed as fallback +if command -v jq &> /dev/null; then + write_progress "Using jq to parse configuration" + # Try to extract services from the devpilot.json using jq + SERVICES_JSON=$(jq -c '.services[]' "$DEVPILOT_CONFIG" 2>/dev/null) + + if [ -z "$SERVICES_JSON" ]; then + write_warning "No services found in DevPilot config or jq command failed" + write_warning "Falling back to default service detection" + # Default fallback + declare -a services=( + "cart:$PROJECT_DIR/cart-cna-microservice:latest" + "products:$PROJECT_DIR/products-cna-microservice:latest" + "search:$PROJECT_DIR/search-cna-microservice:latest" + "store-ui:$PROJECT_DIR/store-ui:latest" + "users:$PROJECT_DIR/users-cna-microservice:latest" + ) + else + # Parse JSON into services array + declare -a services=() + while IFS= read -r service_obj; do + service_name=$(echo "$service_obj" | jq -r '.name') + service_location=$(echo "$service_obj" | jq -r '.location') + # Make relative paths absolute + if [[ "$service_location" != /* ]]; then + service_location="$PROJECT_DIR/$service_location" + fi + services+=("$service_name:$service_location:latest") + write_progress "Detected service: $service_name at $service_location" + done <<< "$SERVICES_JSON" + fi +else + write_warning "jq not found, using fallback method to parse configuration" + # Fallback method using grep and sed if jq is not available + services_section=$(grep -A 100 '"services":' "$DEVPILOT_CONFIG" | grep -B 100 -m 1 '^\s*\]' || grep -A 100 '"services":' "$DEVPILOT_CONFIG") + + declare -a services=() + while IFS= read -r line; do + if [[ "$line" =~ \"name\":\ *\"([^\"]+)\" ]]; then + service_name="${BASH_REMATCH[1]}" + # Try to get the location from the next few lines + location_line=$(grep -A 5 "\"name\": \"$service_name\"" "$DEVPILOT_CONFIG" | grep "\"location\"") + if [[ "$location_line" =~ \"location\":\ *\"([^\"]+)\" ]]; then + service_location="${BASH_REMATCH[1]}" + # Make relative paths absolute + if [[ "$service_location" != /* ]]; then + service_location="$PROJECT_DIR/$service_location" + fi + services+=("$service_name:$service_location:latest") + write_progress "Detected service: $service_name at $service_location" + fi + fi + done <<< "$services_section" + + # If no services detected, use default fallback + if [ ${#services[@]} -eq 0 ]; then + write_warning "No services detected in DevPilot config, using default service detection" + declare -a services=( + "cart:$PROJECT_DIR/cart-cna-microservice:latest" + "products:$PROJECT_DIR/products-cna-microservice:latest" + "search:$PROJECT_DIR/search-cna-microservice:latest" + "store-ui:$PROJECT_DIR/store-ui:latest" + "users:$PROJECT_DIR/users-cna-microservice:latest" + ) + fi +fi + +# Check if registry is running +write_phase "Checking Local Registry" +# Check for either the old kind-registry or the new dev-harbor container +registry_container=$(docker ps --filter "name=kind-registry" --format "{{.Names}}") +harbor_container=$(docker ps --filter "name=dev-harbor" --format "{{.Names}}") + +if [[ -z "$registry_container" && -z "$harbor_container" ]]; then + write_warning "Local registry is not running! Please run setup-kind.sh first." + exit 1 +fi + +if [[ -n "$harbor_container" ]]; then + write_success "Harbor registry is running at $registry_url" +else + write_success "Local registry is running at $registry_url" +fi + +# Process each service +for service_info in "${services[@]}"; do + # Parse service information + IFS=: read -r name path tag <<< "$service_info" + + write_phase "Processing $name Service" + + # Check if directory exists + if [ ! -d "$path" ]; then + write_warning "Directory $path does not exist. Skipping $name service." + continue + fi + + # Check if Dockerfile exists + if [ ! -f "$path/Dockerfile" ]; then + write_warning "Dockerfile not found in $path. Skipping $name service." + continue + fi + + # Build the image + write_step "Building image for $name..." + write_progress "Running docker build in $path" + + pushd "$path" 2>/dev/null + if [ $? -ne 0 ]; then + write_error "Failed to change directory to $path" + continue + fi + + if docker build -t "$name:$tag" .; then + write_success "Built $name:$tag" + + # Tag the image for local registry + write_step "Tagging image for local registry..." + registry_image="$registry_url/$name:$tag" + docker tag "$name:$tag" "$registry_image" + write_success "Tagged as $registry_image" + + # Push to local registry + write_step "Pushing to local registry..." + if docker push "$registry_image"; then + write_success "Successfully pushed $registry_image" + else + write_warning "Failed to push $name: Docker push failed" + fi + else + write_warning "Failed to build $name: Docker build failed" + fi + + # Return to original directory + popd 2>/dev/null +done + +write_phase "Build and Push Summary" +write_success "All services have been processed" +echo -e "\nNext steps:" +echo "1. You can verify images in the registry using: docker images" +echo "2. To see pushed images: curl http://$registry_url/v2/_catalog" \ No newline at end of file diff --git a/.devpilot-scripts/devspace-pipelines.yaml b/.devpilot-scripts/devspace-pipelines.yaml new file mode 100644 index 0000000..5c27c43 --- /dev/null +++ b/.devpilot-scripts/devspace-pipelines.yaml @@ -0,0 +1,183 @@ +version: v2beta1 +name: common-scripts + +vars: + REGISTRY: + default: "image.registry.local:5001" + AZURE_REGISTRY: + default: "kubecondemo.azurecr.io" + IMAGE_TAG: + default: "latest" + # Use sanitized username as default namespace + NAMESPACE: + command: "echo $USER | tr '.' '-'" + # Base directory for Kubernetes manifests + K8S_DIR: + default: ".." + +# Generic images template - will be inherited by services +images: + app: + image: ${REGISTRY}/${SERVICE_NAME} + dockerfile: ./Dockerfile + tags: + - ${IMAGE_TAG} + +# Define reusable pipelines to reduce duplication +pipelines: + # Common pipeline to prepare manifests with namespace replacement + prepare-manifests: + run: | + # Create a temporary directory for the customized yaml + TEMP_DIR=$(mktemp -d) + echo "Creating temporary files in $TEMP_DIR" + echo "export TEMP_DIR=$TEMP_DIR" > ~/.devspace/vars/temp_dir.env + + # Get the base directory + source ~/.devspace/vars/env_type.env + source ~/.devspace/vars/k8s_path.env + MANIFEST_PATH="${K8S_PATH}" + SERVICE_DIR=$(basename "${MANIFEST_PATH}") + BASE_DIR=$(dirname "${MANIFEST_PATH}") + BASE_DIR=$(dirname "$BASE_DIR") + BASE_DIR=$(dirname "$BASE_DIR") + + # Copy the necessary files to the temp directory + mkdir -p $TEMP_DIR/base $TEMP_DIR/overlays/$ENV_TYPE + cp -r ${BASE_DIR}/base/* $TEMP_DIR/base/ + cp -r ${BASE_DIR}/overlays/$ENV_TYPE/* $TEMP_DIR/overlays/$ENV_TYPE/ + + # Save paths for later steps + echo "export SERVICE_DIR=$SERVICE_DIR" >> ~/.devspace/vars/temp_dir.env + echo "export BASE_DIR=$BASE_DIR" >> ~/.devspace/vars/temp_dir.env + + # Replace in all yaml files + find $TEMP_DIR -type f -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/${NAMESPACE}/g" {} \; + + # Create the namespace if it doesn't exist + kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - + + echo "Prepared Kubernetes manifests in $TEMP_DIR with namespace: ${NAMESPACE}" + + # Apply the prepared manifests + apply-manifests: + run: | + source ~/.devspace/vars/temp_dir.env + source ~/.devspace/vars/env_type.env + kubectl apply -k $TEMP_DIR/overlays/$ENV_TYPE/$SERVICE_DIR + + # Handle Istio Gateway for Azure environment + if [[ "$ENV_TYPE" == "azure" ]]; then + # Check if the Istio Gateway exists in the aks-istio-ingress namespace + GATEWAY_EXISTS=$(kubectl get gateway -n aks-istio-ingress store-ui-gateway -o name --ignore-not-found) + + if [ -z "$GATEWAY_EXISTS" ]; then + echo "Creating Istio Gateway in aks-istio-ingress namespace" + # Apply the Gateway configuration separately since it's in a different namespace + GATEWAY_FILE="${BASE_DIR}/overlays/azure/store-ui/store-ui-gateway.yaml" + if [ -f "$GATEWAY_FILE" ]; then + kubectl apply -f "$GATEWAY_FILE" + fi + else + echo "Istio Gateway already exists in aks-istio-ingress namespace" + fi + fi + + # Delete the prepared manifests + delete-manifests: + run: | + source ~/.devspace/vars/temp_dir.env + source ~/.devspace/vars/env_type.env + kubectl delete -k $TEMP_DIR/overlays/$ENV_TYPE/$SERVICE_DIR --ignore-not-found + + # Clean up the temporary directory + cleanup: + run: | + source ~/.devspace/vars/temp_dir.env + rm -rf $TEMP_DIR + echo "Cleaned up temporary directory" + +# Standardized commands for consistency across all microservices +commands: + # Development commands + dev: + command: devspace use namespace ${NAMESPACE} && devspace dev --skip-build --skip-deploy + description: "Start development mode with hot reload" + + # Core deployment commands + deploy: + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=local" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_LOCAL}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline apply-manifests + devspace run-pipeline cleanup + description: "Deploy to local environment with dynamic namespace" + + build: + command: devspace build + description: "Build image for local registry" + + purge: + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=local" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_LOCAL}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline delete-manifests + devspace run-pipeline cleanup + description: "Remove deployment from cluster" + + # Azure profile commands + deploy-azure: + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=azure" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_AZURE}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline apply-manifests + devspace run-pipeline cleanup + description: "Deploy to Azure with dynamic namespace" + + build-azure: + command: devspace build --profile azure + description: "Build image for Azure ACR" + + dev-azure: + command: devspace use namespace ${NAMESPACE} && devspace dev --skip-build --skip-deploy --profile azure + description: "Start development mode with Azure profile" + + purge-azure: + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=azure" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_AZURE}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline delete-manifests + devspace run-pipeline cleanup + description: "Remove Azure deployment from cluster" + +# Azure ACR profile for Kubernetes +profiles: + - name: azure + description: "Azure ACR for Kubernetes deployment" + patches: + - op: replace + path: vars.REGISTRY + value: ${AZURE_REGISTRY} + activation: + - vars: + DEVSPACE_PROFILE: "azure" \ No newline at end of file diff --git a/.devpilot-scripts/env-mgmt.ps1 b/.devpilot-scripts/env-mgmt.ps1 new file mode 100644 index 0000000..b57755c --- /dev/null +++ b/.devpilot-scripts/env-mgmt.ps1 @@ -0,0 +1,24 @@ +# PowerShell script for environment management +param( + [switch]$Start, + [switch]$Stop, + [switch]$Delete, + [switch]$Switch +) + +if ($Start) { + Write-Host "Starting environment..." + # Add your start environment commands here +} +elseif ($Stop) { + Write-Host "Stopping environment..." + # Add your stop environment commands here +} +elseif ($Delete) { + Write-Host "Deleting environment..." + # Add your delete environment commands here +} +elseif ($Switch) { + Write-Host "Switching environment..." + # Add your switch environment commands here +} diff --git a/.devpilot-scripts/env-mgmt.sh b/.devpilot-scripts/env-mgmt.sh new file mode 100755 index 0000000..93b1be4 --- /dev/null +++ b/.devpilot-scripts/env-mgmt.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# Set up the configuration directory within the project's .devpilot-scripts folder +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEVPILOT_CONFIG="${SCRIPT_DIR}/.devpilot-config" +LOG_FILE="${SCRIPT_DIR}/.devpilot-logs/env-mgmt.log" + +# Create necessary directories +mkdir -p "${DEVPILOT_CONFIG}" +mkdir -p "${SCRIPT_DIR}/.devpilot-logs" +mkdir -p "${SCRIPT_DIR}/.devpilot-backups" + +# Log function for consistent logging +log() { + local message="$1" + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + echo -e "${timestamp} - ${message}" >> "${LOG_FILE}" +} + +checkCommandCompatibility(){ + COMMAND=$1 + CURR_CONTEXT=$(kubectl config current-context) + if [[ $CURR_CONTEXT == kind-* ]]; then + log "Cluster mode is local" + elif [[ $CURR_CONTEXT == k8s-* ]]; then + echo -e "\nπŸš€ Current Active cluster is: $CURR_CONTEXT" + echo -e "\n🌐 This cluster is remote." + echo -e "\n '$COMMAND' is only applicable for local kind k8s clusters." + echo -e "\nπŸ’‘ Local kind k8s cluster names starts with kind-* \n" + log "Command '$COMMAND' not compatible with remote cluster $CURR_CONTEXT" + exit 0 + else + log "Unable to determine cluster type for context: $CURR_CONTEXT" + fi +} + +cluster_exists(){ + CURR_CLUSTER_NAME=$1 + + DOCKER_CLUSTER=$(docker ps | awk 'NR==2 {print $NF}') + DOCKER_CLUSTER="${DOCKER_CLUSTER/-control-plane/}" + if [ "$DOCKER_CLUSTER" == "$CURR_CLUSTER_NAME" ]; then + echo -e "\nπŸš€ $CURR_CLUSTER_NAME cluster is already up and running!!\n" + log "Cluster $CURR_CLUSTER_NAME is already running" + exit 1 + fi +} + +cluster_limit_check() { + PREV_CONTEXT=$1 + PREV_CLUSTER_NAME=$2 + + existing_cnt=$(docker ps | grep "control-plane" | wc -l) + if [ "$existing_cnt" -eq 1 ]; then + devspace use context $PREV_CONTEXT >> "${LOG_FILE}" #revert + echo -e "\n❌ Unable to start the cluster" + echo -e "\nReason:\n" + echo -e "πŸ‘‰ Only one kind cluster can be active at a time for optimal system performance" + echo -e "πŸ‘‰ To use a new cluster, kindly stop the existing one and start the new one!!" + echo -e "\nπŸ›‘ Usage: 'devpilot switch-env' to switch to the new cluster." + + echo -e "\nπŸš€ Current Active cluster Name is: $PREV_CLUSTER_NAME \n" + log "Cluster limit reached. Current active cluster: $PREV_CLUSTER_NAME" + exit 1 + fi +} + +start_cluster(){ + + PREV_CONTEXT=$(kubectl config current-context) + PREV_CLUSTER_NAME="${PREV_CONTEXT/kind-/}" + + echo -e "\n\n🌐 Enter the cluster that you want to start?" + devspace use context + checkCommandCompatibility "devspace start-cluster" + CURR_CONTEXT=$(kubectl config current-context) + CLUSTER_NAME="${CURR_CONTEXT/kind-/}" + CONTAINER_NAME="${CLUSTER_NAME}-control-plane" + + cluster_exists $CLUSTER_NAME + cluster_limit_check $PREV_CONTEXT $PREV_CLUSTER_NAME + + echo -e "\n🟒 Starting the cluster: $CLUSTER_NAME with container name: $CONTAINER_NAME" + docker start $CONTAINER_NAME + echo -e "\nβœ… Cluster started successfully!" +} + +stop_cluster(){ + + checkCommandCompatibility "devspace stop-cluster" + CURR_CONTEXT=$(kubectl config current-context) + CLUSTER_NAME="${CURR_CONTEXT/kind-/}" + echo -e "\nπŸš€ Current Active cluster Name is: $CLUSTER_NAME\n" + CONTAINER_NAME="${CLUSTER_NAME}-control-plane" + + read -p "πŸ€” Are you sure you want to stop the cluster $CLUSTER_NAME? (Yes/No): " stop + if [[ "$stop" =~ ^[Yy]es$ ]]; then + echo -e "\nπŸ”΄ Stopping the cluster: $CLUSTER_NAME with container name: $CONTAINER_NAME" + docker stop $CONTAINER_NAME + echo -e "\nπŸ›‘ Cluster is stopped." + else + echo -e "\nπŸ”΅ Operation cancelled by user, not stopping the cluster $CLUSTER_NAME." + exit 1 + fi +} + +switch_cluster(){ + + # Current cluster details + PREV_CONTEXT=$(kubectl config current-context) + PREV_CLUSTER_NAME="${PREV_CONTEXT/kind-/}" + echo -e "\nπŸš€ Current Active cluster Name is: $PREV_CLUSTER_NAME" + + # Prompt to select the new cluster + echo -e "\nπŸ”„ Select the cluster to switch to:" + devspace use context + checkCommandCompatibility "devspace switch-cluster" + CURR_CONTEXT=$(kubectl config current-context) + CURR_CLUSTER_NAME="${CURR_CONTEXT/kind-/}" + + # Check if the selected cluster is already active + if [ "$PREV_CLUSTER_NAME" == "$CURR_CLUSTER_NAME" ]; then + echo -e "\n\nℹ️ The Cluster selected is already active, hence not switching!!" + exit 1 + fi + + # Confirm switch action + echo -e "\n ⚠️ Warning: Switching will stop the current cluster [[ $PREV_CLUSTER_NAME ]] and will spin up the selected cluster [[ $CURR_CLUSTER_NAME ]]!!\n" + read -p "πŸ€” Are you sure you want to switch the cluster? (Yes/No): " switch + + if [[ "$switch" =~ ^[Yy]es$ ]]; then + echo -e "\nπŸ”΄ Stopping the cluster: $PREV_CLUSTER_NAME" + docker stop $PREV_CLUSTER_NAME-control-plane + + echo -e "\n🟒 Starting the cluster: $CURR_CLUSTER_NAME" + docker start $CURR_CLUSTER_NAME-control-plane + devspace use context $CURR_CONTEXT >> ~/.devpilot/tmp.log + + echo -e "\nβœ… Successfully switched to the cluster $CURR_CLUSTER_NAME." + else + # Reverting to previous context if the operation is cancelled + devspace use context $PREV_CONTEXT >> ~/.devpilot/tmp.log + echo -e "\n πŸ”΅ Operation cancelled by user, staying in the same cluster $PREV_CLUSTER_NAME." + fi +} + +delete_cluster(){ + + echo -e "\nπŸ”΄ Select the cluster to delete:" + + devspace use context + CURR_CONTEXT=$(kubectl config current-context) + checkCommandCompatibility "devspace delete-cluster" + CLUSTER_NAME="${CURR_CONTEXT/kind-/}" + echo -e "\nπŸš€ Selected cluster Name is: $CLUSTER_NAME" + CONTAINER_NAME="${CLUSTER_NAME}-control-plane" + + echo -e "\n⚠️ Warning: This will permanently delete the selected cluster [[ $CLUSTER_NAME ]]!!" + echo -e "\nπŸ€” Are you sure you want to delete the cluster $CLUSTER_NAME?" + echo -e " Type 'Yes' to confirm, or press Enter to cancel: \c" + read delete + + # Handle empty input (just pressing Enter) + if [[ -z "$delete" ]]; then + echo -e "\nπŸ”΅ Operation cancelled (no input provided), not deleting the cluster $CLUSTER_NAME." + log "Delete operation cancelled - no input provided" + exit 1 + elif [[ "$delete" =~ ^[Yy]es$ ]]; then + echo -e "\nπŸ”΄ Deleting the cluster: $CLUSTER_NAME with container name: $CONTAINER_NAME" + kind delete cluster --name=$CLUSTER_NAME + echo -e "\nπŸ›‘ Cluster is deleted." + log "Cluster $CLUSTER_NAME deleted" + + # Backup kube config to project-specific location + cp $HOME/.kube/config "${SCRIPT_DIR}/.devpilot-backups/kube-config-$(date +%Y%m%d%H%M%S).bak" + else + echo -e "\nπŸ”΅ Operation cancelled by user, not deleting the cluster $CLUSTER_NAME." + log "Delete operation cancelled by user" + exit 1 + fi + +} + +ssh_cluster(){ + + checkCommandCompatibility "devspace ssh-cluster" + echo -e "\n🟒 Entering into the current active cluster..." + CURR_CONTEXT=$(kubectl config current-context) + CLUSTER_NAME="${CURR_CONTEXT/kind-/}" + echo -e "\nπŸš€ Active cluster Name is: $CLUSTER_NAME\n" + CONTAINER_NAME="${CLUSTER_NAME}-control-plane" + docker exec -it $CONTAINER_NAME bash + exit 0 +} + +validate_input() { + if [[ -z $1 ]]; then + echo "❌ Error: Input cannot be empty." + exit 1 + fi +} + + +main(){ + # Debug logging + log "Received argument: $1" + + arg=$1 + if [[ $arg == "--start-cluster" || $arg == "--start" ]];then + # Accept both --start-cluster and --start for compatibility + log "Starting cluster..." + start_cluster + elif [[ $arg == "--stop-cluster" || $arg == "--stop" ]];then + # Accept both --stop-cluster and --stop for compatibility + log "Stopping cluster..." + stop_cluster + elif [[ $arg == "--switch-cluster" || $arg == "--switch" ]];then + # Accept both --switch-cluster and --switch for compatibility + log "Switching cluster..." + switch_cluster + elif [[ $arg == "--delete-cluster" || $arg == "--delete" ]];then + # Accept both --delete-cluster and --delete for compatibility + log "Deleting cluster..." + delete_cluster + elif [[ $arg == "--ssh-cluster" || $arg == "--ssh" ]];then + # Accept both --ssh-cluster and --ssh for compatibility + log "SSH into cluster..." + ssh_cluster + else + echo "❌ Invalid option: $arg" | tee -a "${LOG_FILE}" + echo "Available options:" | tee -a "${LOG_FILE}" + echo " --start, --start-cluster : Start a cluster" | tee -a "${LOG_FILE}" + echo " --stop, --stop-cluster : Stop a cluster" | tee -a "${LOG_FILE}" + echo " --switch, --switch-cluster : Switch between clusters" | tee -a "${LOG_FILE}" + echo " --delete, --delete-cluster : Delete a cluster" | tee -a "${LOG_FILE}" + echo " --ssh, --ssh-cluster : SSH into a cluster" | tee -a "${LOG_FILE}" + fi +} + +main $@ \ No newline at end of file diff --git a/.devpilot-scripts/install-cluster.ps1 b/.devpilot-scripts/install-cluster.ps1 new file mode 100644 index 0000000..39f557a --- /dev/null +++ b/.devpilot-scripts/install-cluster.ps1 @@ -0,0 +1,3 @@ +# PowerShell script to install cluster +Write-Host "Installing cluster..." +# Add your cluster installation commands here diff --git a/.devpilot-scripts/install-cluster.sh b/.devpilot-scripts/install-cluster.sh new file mode 100755 index 0000000..1c57652 --- /dev/null +++ b/.devpilot-scripts/install-cluster.sh @@ -0,0 +1,366 @@ +#!/bin/bash +#============================================================================== +# Development Environment Setup Script +#============================================================================== +# +# DESCRIPTION: +# Sets up a development environment with: +# 1. A standalone Docker registry container (independent of any cluster) +# 2. A Kind Kubernetes cluster configured to use the Docker registry +# +# USAGE: +# ./install-cluster.sh [CLUSTER_NAME] +# +#============================================================================== + +# Exit on any error +set -e + +# Get the script directory and project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Configuration - support both interactive and non-interactive modes +if [[ -n "$1" ]]; then + cluster_name="$1" + echo "πŸ“› Using cluster name: $cluster_name" +else + echo "πŸ“› Enter a cluster name (or press Enter for 'devpilot-cluster'):" + echo -n " > " + read cluster_name + cluster_name="${cluster_name:-devpilot-cluster}" # Use default if empty + echo "πŸ“› Using cluster name: $cluster_name" +fi + +# Configuration - Using alternative port 9080 instead of 8080 to avoid conflicts +registry_name="kind-registry" +registry_port="5001" # Registry port +registry_ui_port="9080" # Changed UI port to avoid conflict with port 8080 +kind_config_path="$SCRIPT_DIR/kind-cluster-config.yaml" + +echo "πŸ“› Setting up development environment" + +# Helper Functions +function write_phase() { echo -e "\nπŸ“‹ Phase: $1\n"; } +function write_step() { echo -e " $1"; } +function write_progress() { echo -e " β†’ $1"; } +function write_success() { echo -e " βœ“ $1"; } +function write_warning() { echo -e " ⚠ $1"; } +function write_error() { echo -e " ❌ $1"; } + +function start_timer() { start_time=$(date +%s.%N); } +function stop_timer() { + local end_time=$(date +%s.%N) + local elapsed=$(echo "$end_time - $start_time" | bc) + local task_name="$1" + + if (( $(echo "$elapsed >= 60" | bc -l) )); then + local formatted_time=$(echo "scale=1; $elapsed / 60" | bc) + echo -e "⏱️ $task_name completed in ${formatted_time} minutes\n" + elif (( $(echo "$elapsed >= 1" | bc -l) )); then + local formatted_time=$(echo "scale=1; $elapsed" | bc) + echo -e "⏱️ $task_name completed in ${formatted_time} seconds\n" + else + local formatted_time=$(echo "scale=0; $elapsed * 1000" | bc) + echo -e "⏱️ $task_name completed in ${formatted_time} milliseconds\n" + fi +} + +function write_phase_summary() { + local phase="$1" + shift + local completed_steps=("$@") + + echo -e "\nπŸ“ Phase Summary: $phase" + for step in "${completed_steps[@]}"; do + echo -e " βœ“ $step" + done + echo "" +} + +#region Pre-flight Checks +write_phase "Pre-flight Checks" + +write_step "Checking required tools..." +start_timer + +tools=("docker:Docker" "kind:Kind" "kubectl:kubectl") + +for tool_info in "${tools[@]}"; do + cmd="${tool_info%%:*}" + name="${tool_info#*:}" + + if command -v "$cmd" >/dev/null 2>&1; then + write_progress "$name is installed" + else + write_error "$name is not installed or not in PATH" + exit 1 + fi +done + +if [[ ! -f "$kind_config_path" ]]; then + write_error "Kind cluster config file not found at $kind_config_path" + exit 1 +fi + +stop_timer "Pre-flight validation" + +write_phase_summary "Pre-flight Checks" \ + "Required tools verified" \ + "Configuration validated" +#endregion + +#region Phase 1: Docker Registry Setup +write_phase "Setting up Docker Registry" + +write_step "Checking for existing registry container..." +start_timer + +registry_exists=$(docker ps -a --format '{{.Names}}' | grep -E "^${registry_name}$" || true) +if [[ -z "$registry_exists" ]]; then + write_step "Setting up new Docker registry container..." + + # Create Docker volume for registry data persistence + write_progress "Creating Docker volume for registry data..." + docker volume create registry-data || true + + # Pull and run the Docker registry + write_progress "Starting registry container..." + + # Using registry:2 image with CORS enabled + docker run -d \ + --name $registry_name \ + -p $registry_port:5000 \ + -v registry-data:/var/lib/registry \ + -e REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin="['*']" \ + -e REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods="['HEAD', 'GET', 'OPTIONS', 'DELETE']" \ + -e REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers="['Authorization', 'Accept', 'Content-Type']" \ + -e REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers="['Docker-Content-Digest']" \ + --restart always \ + registry:2 + + # Set up the registry UI + write_progress "Starting registry UI container..." + docker run -d \ + --name registry-ui \ + -p $registry_ui_port:80 \ + -e REGISTRY_URL=http://localhost:$registry_port \ + -e SINGLE_REGISTRY=true \ + -e DELETE_IMAGES=true \ + -e REGISTRY_TITLE="Docker Registry UI" \ + --restart=always \ + joxit/docker-registry-ui:latest + + write_success "Docker registry and UI successfully created and running" +else + write_progress "Registry container already exists, checking status..." + health=$(docker inspect --format='{{.State.Status}}' "$registry_name") + if [[ "$health" != "running" ]]; then + write_progress "Starting existing registry container..." + docker start "$registry_name" + fi + + # Check for UI container + ui_exists=$(docker ps -a --format '{{.Names}}' | grep -E "^registry-ui$" || true) + if [[ -z "$ui_exists" ]]; then + write_progress "Starting registry UI container..." + docker run -d \ + --name registry-ui \ + -p $registry_ui_port:80 \ + -e REGISTRY_URL=http://localhost:$registry_port \ + -e SINGLE_REGISTRY=true \ + -e DELETE_IMAGES=true \ + -e REGISTRY_TITLE="Docker Registry UI" \ + --restart=always \ + joxit/docker-registry-ui:latest + else + ui_health=$(docker inspect --format='{{.State.Status}}' "registry-ui" 2>/dev/null || echo "not_found") + if [[ "$ui_health" != "running" ]]; then + write_progress "Starting existing UI container..." + docker start "registry-ui" + fi + fi + + write_success "Docker registry and UI containers are running" +fi + +# Add to /etc/hosts if not already there +if ! grep -q "image.registry.local" /etc/hosts; then + write_progress "Adding image.registry.local to /etc/hosts..." + echo "127.0.0.1 image.registry.local" | sudo tee -a /etc/hosts + write_success "Added image.registry.local to /etc/hosts" +else + write_progress "image.registry.local already in /etc/hosts" +fi + +stop_timer "Registry setup" + +write_phase_summary "Docker Registry Setup" \ + "Standalone registry running" \ + "Registry accessible at image.registry.local:$registry_port" \ + "Registry UI accessible at http://localhost:$registry_ui_port" \ + "Data persistence configured with Docker volumes" +#endregion + +#region Phase 2: Cluster Creation +write_phase "Creating Kind Cluster" + +write_step "Checking cluster status..." +start_timer +existing_clusters=$(kind get clusters) + +if echo "$existing_clusters" | grep -q "^${cluster_name}$"; then + write_progress "Cluster ${cluster_name} exists. Deleting for fresh setup..." + kind delete cluster --name "$cluster_name" + write_success "Existing cluster deleted successfully" +else + write_progress "Cluster ${cluster_name} does not exist. Creating new cluster..." +fi + +# Clean up any leftover containers +write_progress "Cleaning up leftover containers..." +docker rm -f "${cluster_name}-control-plane" "${cluster_name}-worker" 2>/dev/null || true + +# Give time for cleanup +sleep 3 + +write_step "Creating Kubernetes cluster..." + +# Create cluster with better error handling +if ! kind create cluster --name "$cluster_name" --config "$kind_config_path"; then + write_error "Failed to create cluster $cluster_name" + exit 1 +fi + +write_success "Cluster created successfully" + +# Quick verification +write_progress "Verifying cluster..." +if kubectl get nodes >/dev/null 2>&1; then + write_success "Cluster is accessible" +else + write_warning "Cluster created but not immediately accessible" +fi + +stop_timer "Cluster creation" + +write_phase_summary "Cluster Creation" \ + "Old resources cleaned up" \ + "New cluster created: $cluster_name" \ + "Configuration applied" +#endregion + +#region Phase 3: Registry Integration +write_phase "Configuring Registry Access" + +write_step "Setting up registry access in cluster nodes..." +start_timer + +# Configure cluster to use Docker registry +write_progress "Configuring cluster to use Docker registry..." + +# Get nodes with timeout +write_progress "Getting cluster nodes..." +retry_count=0 +max_retries=10 + +while true; do + kind_nodes=$(kind get nodes --name "$cluster_name" 2>/dev/null || true) + + if [[ -n "$kind_nodes" ]]; then + node_count=$(echo "$kind_nodes" | wc -l) + write_success "Found $node_count cluster node(s)" + break + fi + + retry_count=$((retry_count + 1)) + if [[ $retry_count -eq $max_retries ]]; then + write_error "Could not get cluster nodes after $max_retries attempts" + exit 1 + fi + + write_progress "Waiting for nodes... (Attempt $retry_count/$max_retries)" + sleep 2 +done + +# Configure each node to trust the Docker registry +reg_host_dir="/etc/containerd/certs.d/image.registry.local:$registry_port" +echo "$kind_nodes" | while read -r node; do + if [[ -n "$node" ]]; then + write_progress "Configuring node: $node" + docker exec "$node" mkdir -p "$reg_host_dir" + hosts_toml="[host.\"http://image.registry.local:$registry_port\"]" + echo "$hosts_toml" | docker exec -i "$node" /bin/sh -c "cat > $reg_host_dir/hosts.toml" + write_success "Node $node configured" + fi +done + +# Connect registry container to kind network +write_progress "Connecting registry to cluster network..." +docker network connect "kind" "$registry_name" || true +write_progress "Connecting UI container to cluster network..." +docker network connect "kind" "registry-ui" || true +write_success "Networks connected" + +stop_timer "Registry integration" + +write_phase_summary "Registry Integration" \ + "Registry linked to Kind cluster" \ + "Network connectivity established" +#endregion + +#region Final Setup +write_phase "Final Configuration" + +write_step "Creating Kubernetes registry configuration..." +start_timer + +# Wait for API to be ready +write_progress "Waiting for Kubernetes API..." +kubectl wait --for=condition=Ready nodes --all --timeout=60s + +# Create registry ConfigMap +config_map=$(cat </dev/null 2>&1 +echo "$config_map" | kubectl apply -f - + +write_success "Registry ConfigMap created" + +stop_timer "Final configuration" +#endregion + +#region Summary +echo "" +echo "✨ Development environment setup complete! ✨" +echo "" +echo "πŸ”Έ Cluster: $cluster_name" +echo "πŸ”Έ Nodes: $(kubectl get nodes --no-headers | wc -l)" +echo "" +echo "🌟 Docker Registry Information 🌟" +echo "πŸ”Έ Registry: image.registry.local:$registry_port" +echo "πŸ”Έ Registry UI: http://localhost:$registry_ui_port" +echo "πŸ”Έ Access credentials: Not required (open access)" +echo "" +echo "πŸ”Έ To push images to the registry:" +echo " docker tag your-image:tag image.registry.local:$registry_port/your-image:tag" +echo " docker push image.registry.local:$registry_port/your-image:tag" +echo "" +echo "πŸ”Έ Quick test:" +echo " kubectl get nodes" +echo " curl http://image.registry.local:$registry_port/v2/_catalog" +echo "" +echo "πŸ’‘ Registry data is persisted in Docker volumes. Your registry data will be" +echo " preserved even when you stop or delete your Kubernetes cluster." +#endregion \ No newline at end of file diff --git a/.devpilot-scripts/install.ps1 b/.devpilot-scripts/install.ps1 new file mode 100644 index 0000000..5e246f2 --- /dev/null +++ b/.devpilot-scripts/install.ps1 @@ -0,0 +1,18 @@ +# PowerShell script to install components +param( + [switch]$Infra, + [switch]$App +) + +if ($Infra) { + Write-Host "Installing infrastructure..." + # Add your infrastructure installation commands here +} +elseif ($App) { + Write-Host "Installing application..." + # Add your application installation commands here +} +else { + Write-Host "Installing both infrastructure and application..." + # Add your complete installation commands here +} diff --git a/.devpilot-scripts/install.sh b/.devpilot-scripts/install.sh new file mode 100755 index 0000000..492623d --- /dev/null +++ b/.devpilot-scripts/install.sh @@ -0,0 +1,117 @@ +#!/bin/bash +#============================================================================== +# E-Commerce Microservices Deployment Script +#============================================================================== +# +# DESCRIPTION: +# Simple script to deploy the e-commerce microservices to a Kubernetes cluster +# with namespace substitution while maintaining proper directory structure +# for kustomize to resolve base references correctly. +# +#============================================================================== + +# Helper Functions +function write_phase() { echo -e "\nπŸš€ $1"; } +function write_step() { echo -e "\nπŸ“‹ $1"; } +function write_success() { echo -e "βœ… $1"; } +function write_warning() { echo -e "⚠️ $1"; } +function write_error() { echo -e "❌ $1"; } + +# Get base path from devpilot.json +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +DEVPILOT_CONFIG="$PROJECT_DIR/.devpilot.json" + +# Set default base path +BASE_PATH="$PROJECT_DIR/infra/k8s" + +# Get base path from devpilot.json if available +if [ -f "$DEVPILOT_CONFIG" ] && command -v jq &> /dev/null; then + CUSTOM_PATH=$(jq -r '.deployment_manifests_location // ""' "$DEVPILOT_CONFIG") + if [ -n "$CUSTOM_PATH" ]; then + # Make path absolute if relative + if [[ "$CUSTOM_PATH" != /* ]]; then + BASE_PATH="$PROJECT_DIR/$CUSTOM_PATH" + else + BASE_PATH="$CUSTOM_PATH" + fi + fi +fi + +# Parse command line arguments +MODE="both" +PARAMS=() + +while (( "$#" )); do + case "$1" in + --infra) MODE="infra"; shift ;; + --app) MODE="app"; shift ;; + *) PARAMS+=("$1"); shift ;; + esac +done + +# Restore positional parameters +set -- "${PARAMS[@]}" + +# Use current username as default namespace +DEFAULT_NAMESPACE=$(echo "$USER" | tr '.' '-') +USER_NAMESPACE=${1:-$DEFAULT_NAMESPACE} +ENVIRONMENT=${2:-"local"} # Default to local if not specified + +# Create namespace if it doesn't exist +kubectl create namespace $USER_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - + +# Deploy infrastructure if requested +if [ "$MODE" == "infra" ] || [ "$MODE" == "both" ]; then + write_phase "Setting up infrastructure..." + INFRA_PATH="$BASE_PATH/shared-services/overlays/$ENVIRONMENT" + + if [ -d "$INFRA_PATH" ]; then + write_step "Applying infrastructure from $INFRA_PATH" + kubectl apply -k "$INFRA_PATH" && write_success "Infrastructure deployed successfully" || write_error "Failed to deploy infrastructure" + else + write_warning "Infrastructure directory not found at: $INFRA_PATH" + fi +fi + +# Deploy application if requested +if [ "$MODE" == "app" ] || [ "$MODE" == "both" ]; then + write_phase "Setting up application..." + APP_OVERLAY_PATH="$BASE_PATH/apps/overlays/$ENVIRONMENT" + APP_BASE_PATH="$BASE_PATH/apps/base" + + if [ -d "$APP_OVERLAY_PATH" ]; then + # Create a temp directory that preserves the directory structure + TEMP_DIR=$(mktemp -d) + write_step "Created temporary directory: $TEMP_DIR" + + # Create the proper directory structure in the temp directory + mkdir -p "$TEMP_DIR/apps/overlays/$ENVIRONMENT" + mkdir -p "$TEMP_DIR/apps/base" + + # Copy the overlay and base directories + cp -r "$APP_OVERLAY_PATH"/* "$TEMP_DIR/apps/overlays/$ENVIRONMENT/" + if [ -d "$APP_BASE_PATH" ]; then + cp -r "$APP_BASE_PATH"/* "$TEMP_DIR/apps/base/" + fi + + # Replace namespace placeholder in all yaml files + find "$TEMP_DIR" -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + + # Apply with kustomize + write_step "Applying application manifests with kustomize..." + kubectl apply -k "$TEMP_DIR/apps/overlays/$ENVIRONMENT" && write_success "Application deployed successfully with kustomize" || { + write_warning "Kustomize failed, applying YAML files directly" + find "$TEMP_DIR/apps/overlays/$ENVIRONMENT" -name "*.yaml" ! -name "kustomization.yaml" -exec kubectl apply -f {} \; + } + + # Clean up + rm -rf "$TEMP_DIR" + else + write_warning "Application directory not found at: $APP_OVERLAY_PATH" + fi + + write_success "Application setup completed" +fi + +write_success "Environment setup completed successfully!" diff --git a/infra/kind-cluster-config.yaml b/.devpilot-scripts/kind-cluster-config.yaml similarity index 99% rename from infra/kind-cluster-config.yaml rename to .devpilot-scripts/kind-cluster-config.yaml index ab4a84d..b801162 100644 --- a/infra/kind-cluster-config.yaml +++ b/.devpilot-scripts/kind-cluster-config.yaml @@ -1,4 +1,4 @@ -ο»Ώkind: Cluster +kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: e-commerce-cluster diff --git a/.devpilot-scripts/server-mgmt.ps1 b/.devpilot-scripts/server-mgmt.ps1 new file mode 100644 index 0000000..28929ce --- /dev/null +++ b/.devpilot-scripts/server-mgmt.ps1 @@ -0,0 +1,29 @@ +# PowerShell script for server management +param( + [switch]$Run, + [switch]$Stop, + [switch]$Build, + [switch]$Deploy, + [switch]$Test +) + +if ($Run) { + Write-Host "Running server..." + # Add your run server commands here +} +elseif ($Stop) { + Write-Host "Stopping server..." + # Add your stop server commands here +} +elseif ($Build) { + Write-Host "Building server..." + # Add your build server commands here +} +elseif ($Deploy) { + Write-Host "Deploying server..." + # Add your deploy server commands here +} +elseif ($Test) { + Write-Host "Testing server..." + # Add your test server commands here +} diff --git a/.devpilot-scripts/server-mgmt.sh b/.devpilot-scripts/server-mgmt.sh new file mode 100755 index 0000000..336f9dd --- /dev/null +++ b/.devpilot-scripts/server-mgmt.sh @@ -0,0 +1,467 @@ +#!/bin/bash +# DevPilot Server Management Script for Linux/macOS +# Handles all dev server lifecycle operations + +DEVPILOT_CONFIG="$HOME/.config/devpilot/" + +checkDevSpaceInstalled(){ + if ! command -v devspace >/dev/null 2>&1; then + echo "❌ 'devspace' command not found. Please install DevSpace CLI first." + echo "πŸ’‘ Install from: https://devspace.sh/cli/docs/getting-started/installation" + exit 1 + fi +} + +setup_server(){ + echo "πŸ› οΈ Setting up dev server..." + + checkDevSpaceInstalled + + # TODO: Complex logic - implement manually + # This should run devspace init and handle the interactive setup + + echo "⚠️ Setup-server command not yet implemented - placeholder only" + echo "πŸ“‹ This command will run 'devspace init' to generate devspace.yaml" + echo "πŸ’‘ For now, run 'devspace init' manually in your project directory" +} + +run_server(){ + echo "πŸš€ Starting dev server..." + + local profile="" + local namespace="" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --profile) + profile="$2" + shift 2 + ;; + --namespace) + namespace="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace run dev" + + if [[ -n "$profile" ]]; then + cmd="$cmd --profile $profile" + echo "πŸ“‹ Using profile: $profile" + fi + + if [[ -n "$namespace" ]]; then + cmd="$cmd --namespace $namespace" + echo "🎯 Target namespace: $namespace" + fi + + echo "πŸ”„ Executing: $cmd" + + # Execute the command and handle interruption + trap 'echo -e "\nπŸ‘‹ Dev server stopped"; exit 0' SIGINT + eval $cmd + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + echo "❌ Dev server failed to start" + exit $exit_code + fi +} + +debug_server(){ + echo "πŸ› Starting dev server in debug mode..." + + local profile="" + local namespace="" + local debug_port="" + local verbose_output=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + "--profile") + profile="$2" + shift 2 + ;; + "--namespace") + namespace="$2" + shift 2 + ;; + "--debug-port") + debug_port="$2" + shift 2 + ;; + "--verbose-output") + verbose_output=true + shift + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace run dev" + + if [[ -n "$profile" ]]; then + cmd="$cmd --profile $profile" + echo "πŸ“‹ Using profile: $profile" + fi + + if [[ -n "$namespace" ]]; then + cmd="$cmd --namespace $namespace" + echo "🎯 Target namespace: $namespace" + fi + + if [[ -n "$debug_port" ]]; then + cmd="$cmd --port-forwarding localhost:$debug_port:$debug_port" + echo "πŸ” Debug port forwarding: $debug_port" + fi + + if [[ "$verbose_output" == "true" ]]; then + cmd="$cmd --verbose" + echo "πŸ“ Verbose logging enabled" + fi + + echo "πŸ”„ Executing: $cmd" + + # Execute the command and handle interruption + trap 'echo -e "\nπŸ‘‹ Debug server stopped"; exit 0' SIGINT + eval $cmd + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + echo "❌ Debug server failed to start" + exit $exit_code + fi +} + +build_server(){ + echo "πŸ”¨ Building server images..." + + local profile="" + local tag="" + local skip_push=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --profile) + profile="$2" + shift 2 + ;; + --tag) + tag="$2" + shift 2 + ;; + --skip-push) + skip_push=true + shift + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace run build" + + if [[ -n "$profile" ]]; then + cmd="$cmd --profile $profile" + echo "πŸ“‹ Using profile: $profile" + fi + + if [[ -n "$tag" ]]; then + cmd="$cmd --tag $tag" + echo "🏷️ Using tag: $tag" + fi + + if [[ "$skip_push" == "true" ]]; then + cmd="$cmd --skip-push" + echo "πŸ“¦ Skipping image push" + fi + + echo "πŸ”„ Executing: $cmd" + eval $cmd + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + echo "βœ… Server images built successfully" + else + echo "❌ Server image build failed" + exit $exit_code + fi +} + +deploy_server(){ + echo "πŸš€ Deploying server to cluster..." + + local profile="" + local namespace="" + local skip_build=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --profile) + profile="$2" + shift 2 + ;; + --namespace) + namespace="$2" + shift 2 + ;; + --skip-build) + skip_build=true + shift + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace run deploy" + + if [[ -n "$profile" ]]; then + cmd="$cmd --profile $profile" + echo "πŸ“‹ Using profile: $profile" + fi + + if [[ -n "$namespace" ]]; then + cmd="$cmd --namespace $namespace" + echo "🎯 Target namespace: $namespace" + fi + + if [[ "$skip_build" == "true" ]]; then + cmd="$cmd --skip-build" + echo "πŸ“¦ Skipping image build" + fi + + echo "πŸ”„ Executing: $cmd" + eval $cmd + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + echo "βœ… Server deployed successfully" + else + echo "❌ Server deployment failed" + exit $exit_code + fi +} + +test_server(){ + echo "πŸ§ͺ Testing server..." + + local profile="" + local pipeline="test-server" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --profile) + profile="$2" + shift 2 + ;; + --pipeline) + pipeline="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + if [[ -n "$profile" ]]; then + echo "πŸ“‹ Using profile: $profile" + fi + + echo "πŸ”¬ Test pipeline: $pipeline" + + # TODO: Complex logic - implement manually + # This should run custom devspace pipelines for testing + + echo "⚠️ Test-server command not yet implemented - placeholder only" + echo "πŸ“‹ This command will run custom devspace test pipelines" + echo "πŸ’‘ For now, run 'devspace run test-server' manually" +} + +get_logs(){ + echo "πŸ“‹ Getting server logs..." + + local service="" + local follow=false + local lines="" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --service) + service="$2" + shift 2 + ;; + --follow) + follow=true + shift + ;; + --lines) + lines="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace logs" + + if [[ -n "$service" ]]; then + cmd="$cmd --container $service" + echo "🎯 Service: $service" + fi + + if [[ "$follow" == "true" ]]; then + cmd="$cmd --follow" + echo "πŸ‘οΈ Following logs..." + fi + + if [[ -n "$lines" ]]; then + cmd="$cmd --lines $lines" + echo "πŸ“ Showing $lines lines" + fi + + # Execute the command and handle interruption + trap 'echo -e "\nπŸ‘‹ Stopped following logs"; exit 0' SIGINT + eval $cmd + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + echo "❌ Failed to get logs" + exit $exit_code + fi +} + +open_shell(){ + echo "🐚 Opening shell in server container..." + + local service="" + local container="" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --service) + service="$2" + shift 2 + ;; + --container) + container="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + checkDevSpaceInstalled + + local cmd="devspace enter" + + if [[ -n "$service" ]]; then + cmd="$cmd --container $service" + echo "🎯 Service: $service" + fi + + if [[ -n "$container" ]]; then + cmd="$cmd --container $container" + echo "πŸ“¦ Container: $container" + fi + + # Execute the command and handle interruption + trap 'echo -e "\nπŸ‘‹ Exited shell"; exit 0' SIGINT + eval $cmd + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + echo "❌ Failed to open shell" + exit $exit_code + fi +} + +get_status(){ + echo "πŸ“Š Checking server status..." + + checkDevSpaceInstalled + + local cmd="devspace list deployments" + eval $cmd + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + echo "βœ… Server status retrieved" + else + echo "❌ Failed to get server status" + exit $exit_code + fi +} + +# Main execution logic +main(){ + local action=$1 + shift # Remove action from arguments + + case $action in + "--setup-server") + setup_server "$@" + ;; + "--run-server") + run_server "$@" + ;; + "--debug-server") + debug_server "$@" + ;; + "--build-server") + build_server "$@" + ;; + "--deploy-server") + deploy_server "$@" + ;; + "--test-server") + test_server "$@" + ;; + "--logs") + get_logs "$@" + ;; + "--shell") + open_shell "$@" + ;; + "--status") + get_status "$@" + ;; + *) + echo "Invalid option: $action" + echo "Available options: --setup-server, --run-server, --debug-server, --build-server, --deploy-server, --test-server, --logs, --shell, --status" + exit 1 + ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/.devpilot-scripts/uninstall.ps1 b/.devpilot-scripts/uninstall.ps1 new file mode 100644 index 0000000..33370ac --- /dev/null +++ b/.devpilot-scripts/uninstall.ps1 @@ -0,0 +1,3 @@ +# PowerShell script to uninstall components +Write-Host "Uninstalling application..." +# Add your uninstall commands here diff --git a/.devpilot-scripts/uninstall.sh b/.devpilot-scripts/uninstall.sh new file mode 100755 index 0000000..1cdea7b --- /dev/null +++ b/.devpilot-scripts/uninstall.sh @@ -0,0 +1,156 @@ +#!/bin/bash +#============================================================================== +# E-Commerce Microservices Uninstall Script +#============================================================================== +# +# DESCRIPTION: +# Simple script to uninstall the e-commerce microservices from a Kubernetes +# cluster with a single command approach. +# +# USAGE: +# ./uninstall.sh [NAMESPACE] [ENVIRONMENT] [--infra|--app] +# +# ARGUMENTS: +# NAMESPACE - Kubernetes namespace to remove from (defaults to current username) +# ENVIRONMENT - Target environment (local, dev, prod, etc.) (defaults to "local") +# +# OPTIONS: +# --infra - Remove only shared infrastructure components +# --app - Remove only application microservices +# +#============================================================================== + +# Helper Functions +function write_phase() { echo -e "\nπŸš€ $1"; } +function write_step() { echo -e "\nπŸ“‹ $1"; } +function write_success() { echo -e "βœ… $1"; } +function write_warning() { echo -e "⚠️ $1"; } +function write_error() { echo -e "❌ $1"; } +function write_trash() { echo -e "πŸ—‘οΈ $1"; } +function write_info() { echo -e "ℹ️ $1"; } + +# Get base path from devpilot.json +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +DEVPILOT_CONFIG="$PROJECT_DIR/.devpilot.json" + +# Set default base path +BASE_PATH="$PROJECT_DIR/infra/k8s" + +# Get base path from devpilot.json if available +if [ -f "$DEVPILOT_CONFIG" ] && command -v jq &> /dev/null; then + CUSTOM_PATH=$(jq -r '.deployment_manifests_location // ""' "$DEVPILOT_CONFIG") + if [ -n "$CUSTOM_PATH" ]; then + # Make path absolute if relative + if [[ "$CUSTOM_PATH" != /* ]]; then + BASE_PATH="$PROJECT_DIR/$CUSTOM_PATH" + else + BASE_PATH="$CUSTOM_PATH" + fi + fi +fi + +# Parse command line arguments +MODE="both" +PARAMS=() + +while (( "$#" )); do + case "$1" in + --infra) MODE="infra"; shift ;; + --app) MODE="app"; shift ;; + *) PARAMS+=("$1"); shift ;; + esac +done + +# Restore positional parameters +set -- "${PARAMS[@]}" + +# Use current username as default namespace +DEFAULT_NAMESPACE=$(echo "$USER" | tr '.' '-') +USER_NAMESPACE=${1:-$DEFAULT_NAMESPACE} +ENVIRONMENT=${2:-"local"} # Default to local if not specified + +# Track start time +START_TIME=$(date +%s) + +write_trash "Starting uninstallation from namespace: $USER_NAMESPACE" +write_info "Environment: $ENVIRONMENT" + +# Uninstall application if requested +if [ "$MODE" == "app" ] || [ "$MODE" == "both" ]; then + write_phase "Uninstalling application components..." + APP_PATH="$BASE_PATH/apps/overlays/$ENVIRONMENT" + + if [ -d "$APP_PATH" ]; then + # Create a temp directory for namespace substitution + TEMP_DIR=$(mktemp -d) + write_step "Created temporary directory: $TEMP_DIR" + + # Create the proper directory structure in the temp directory + mkdir -p "$TEMP_DIR/apps/overlays/$ENVIRONMENT" + mkdir -p "$TEMP_DIR/apps/base" + + # Copy the overlay and base directories + cp -r "$APP_PATH"/* "$TEMP_DIR/apps/overlays/$ENVIRONMENT/" 2>/dev/null + + APP_BASE_PATH="$BASE_PATH/apps/base" + if [ -d "$APP_BASE_PATH" ]; then + cp -r "$APP_BASE_PATH"/* "$TEMP_DIR/apps/base/" 2>/dev/null + fi + + # Replace namespace placeholder in all yaml files + find "$TEMP_DIR" -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; 2>/dev/null + + # Single command to uninstall all application components + write_trash "Uninstalling all application components with a single command..." + kubectl delete -k "$TEMP_DIR/apps/overlays/$ENVIRONMENT" --ignore-not-found && + write_success "All application components uninstalled" || + write_warning "Some application components may not have been uninstalled completely" + + # Clean up + rm -rf "$TEMP_DIR" + else + write_warning "Application directory not found at: $APP_PATH" + fi + + write_success "Application uninstallation completed" +fi + +# Uninstall infrastructure if requested +if [ "$MODE" == "infra" ] || [ "$MODE" == "both" ]; then + write_phase "Uninstalling shared infrastructure..." + INFRA_PATH="$BASE_PATH/shared-services/overlays/$ENVIRONMENT" + + if [ -d "$INFRA_PATH" ]; then + write_step "Removing infrastructure from $INFRA_PATH" + kubectl delete -k "$INFRA_PATH" --ignore-not-found && + write_success "Infrastructure removed" || + write_error "Failed to remove infrastructure" + else + write_warning "Infrastructure directory not found at: $INFRA_PATH" + fi +fi + +# If app mode or both, remove the namespace +if [ "$MODE" == "app" ] || [ "$MODE" == "both" ]; then + write_trash "Removing namespace: $USER_NAMESPACE" + kubectl delete namespace "$USER_NAMESPACE" --ignore-not-found + write_success "Namespace removed" +fi + +# Calculate elapsed time +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +MINUTES=$((ELAPSED / 60)) +SECONDS=$((ELAPSED % 60)) + +write_success "Uninstallation completed in ${MINUTES}m ${SECONDS}s" + +if [ "$MODE" == "app" ]; then + write_info "Only application components were removed" + write_info "Shared infrastructure is preserved" + write_info "To remove shared infrastructure, run with: $0 $USER_NAMESPACE $ENVIRONMENT --infra" +elif [ "$MODE" == "infra" ]; then + write_info "Only shared infrastructure was removed" + write_info "To remove application components, run with: $0 $USER_NAMESPACE $ENVIRONMENT --app" +fi \ No newline at end of file diff --git a/.devpilot.json b/.devpilot.json new file mode 100644 index 0000000..05ea850 --- /dev/null +++ b/.devpilot.json @@ -0,0 +1,37 @@ +{ + "project_name": "e-commerce-microservices-sample", + "config": { + "root_location": "/home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample" + }, + "mono_repo": "yes", + "services": [ + { + "name": "users", + "location": "users-cna-microservice" + }, + { + "name": "search", + "location": "search-cna-microservice" + }, + { + "name": "products", + "location": "products-cna-microservice" + }, + { + "name": "cart", + "location": "cart-cna-microservice" + }, + { + "name": "store_ui", + "location": "store-ui" + } + ], + "deployment_manifests_location": "infra/k8s/", + "helper_scripts_location": ".devpilot-scripts", + "devDependencies": [ + "docker", + "kind", + "devspace", + "kubectl" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index c94400d..2933924 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,20 @@ -# Sample E-Commerce application using Microservices / Cloud Native Architecture (CNA) -**_A fictitious e-commerce sample application built using modern technologies and Microservices & Cloud Native Architecture patterns._** -- **Polyglot Languages & Frameworks** (Java - Spring Boot/Cloud, Python - FastAPI, SQLAlachamey, JavaScript/TypeScript - Node, ExpressJS, React) -- **Polyglot Databases** (MongoDB, Redis, ElastiSearch, PostgreSQL) -- Able to deploy to **local Kubernetes (k8s) cluster** as containers (Docker) and also to **Public Cloud (AWS)**. +# e-commerce-microservices-sample -This is an end-to-end **e-commerce solution** that demonstrates how to build a moder CNA application using microservices architecture with full-stack technologies. This application includes below functional microservices which are independently deployable with bounded context. - -_You can standup application locally on your laptop/desktop with few steps and also to AWS._ - -## App - UI/UX, Architecture & Technologies Used - -Architecture | Application UI/UX -:-------------------------:|:-------------------------: -Architecture | Application UI - -## Functional Microservices -| Microservice | Description | Technologies Used | -| --- | --- | --- | -| [Product Catalog Microservice](products-cna-microservice/README.md) | Provides e-commerce merchandise information and images. | A REST API built using NodeJS, ExpressJS relies MongoDB as a data store. | -| [Shopping Cart Microservice](cart-cna-microservice/README.md) | A Microservice with shopping cart and checkout features. | A REST API built using Spring Boot & Cloud with Gradle as build tool, leverages Redis as in-memory data store. | -| [User Profile Microservice](users-cna-microservice/README.md) | User profile management, account and more. | A REST API built using Python FastAPI and SQLAlchamey used PostreSQL | -| [Search Microservice](search-cna-microservice/README.md) | Enables seach functionality such as auto complete, typeahead, faceted search features | A proxy to ElasticSearch, leverages Node| -| [Store UI](store-ui/README.md) | A web UI frontend for e-commerce store that uses above Microservices | A web app built using React, Material UI using TypeScript/JavaScript| - -## Folder Structure -```bash -. -β”œβ”€β”€ store-ui # Web Store Ract App with Material UI -β”‚ └── ... -β”œβ”€β”€ cart-cna-microservice # Shopping Cart Microservice repository -β”œβ”€β”€ products-cna-microservice # Product Catalog Microservice folder -β”œβ”€β”€ search-cna-microservice # Search Microservice -β”œβ”€β”€ users-cna-microservice # User Profile Management Microservice -β”œβ”€β”€ users-cna-microservice # User Profile Management Microservice -β”œβ”€β”€ store-ui # Web Store Ract App with Material UI -└── infra # Infrastructure scripts to setup app locally & cloud - β”œβ”€β”€ k8s # Kubernetes (k8s) YAML files - β”‚ └── apps # Microservices related k8s yaml files. - β”‚ └── shared-services # Databases, ElasticSeach related k8s yaml files. - β”œβ”€β”€ terraform # Terraform scripts to deploy to AWS - └── performance # Performance and load testing scripts -``` +This project is managed with DevPilot. ## Getting Started -### Build -Go through detailed instructions specified in README.md file of each microservice. +1. Install dependencies: `devpilot setup-env` +2. Start the environment: `devpilot start-env` +3. Run the server: `devpilot run-server` -### Deploy -Refer to [instructions](infra/README.md) to deploy application and dependent services such as MongoDB, Redis, ... either to local machine or AWS. +## Available Commands -## Issues & Feedback -Raise an issue in Github. Will address as soon as possible. +- `devpilot setup-env`: Setup the development environment +- `devpilot start-env`: Start the development environment +- `devpilot stop-env`: Stop the development environment +- `devpilot delete-env`: Delete the development environment +- `devpilot run-server`: Run the development server +- `devpilot build-server`: Build the server +- `devpilot deploy-server`: Deploy the server +- `devpilot test-server`: Run tests diff --git a/cart-cna-microservice/devspace.yaml b/cart-cna-microservice/devspace.yaml index c5ca013..7866f7d 100644 --- a/cart-cna-microservice/devspace.yaml +++ b/cart-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: cart -images: - cart: - image: image.registry.local:5001/cart - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../.devpilot-scripts/devspace-pipelines.yaml" -deployments: - cart: - kubectl: - manifests: - - ../infra/k8s/apps/overlays/local/cart/ - kustomize: true +vars: + SERVICE_NAME: + default: "cart" + K8S_MANIFEST_PATH_LOCAL: + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/cart/" + K8S_MANIFEST_PATH_AZURE: + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/cart/" dev: cart: @@ -47,14 +44,4 @@ dev: - name: APP_ENVIRONMENT value: "development" - name: LOG_LEVEL_ROOT - value: "INFO" - -commands: - start: - command: devspace enter cart -- ./gradlew bootRun - build: - command: devspace enter cart -- ./gradlew build - test: - command: devspace enter cart -- ./gradlew test - clean: - command: devspace enter cart -- ./gradlew clean \ No newline at end of file + value: "INFO" \ No newline at end of file diff --git a/infra/build-and-push-images.ps1 b/infra/build-and-push-images.ps1 deleted file mode 100644 index adbb945..0000000 --- a/infra/build-and-push-images.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -# Helper Functions -function Write-Phase { - param([string]$phase) - Write-Host "`nπŸš€ $phase" -ForegroundColor Cyan -} - -function Write-Step { - param([string]$step) - Write-Host "`nπŸ“‹ $step" -ForegroundColor Yellow -} - -function Write-Progress { - param([string]$message) - Write-Host " $message" -ForegroundColor Gray -} - -function Write-Success { - param([string]$message) - Write-Host "βœ… $message" -ForegroundColor Green -} - -function Write-Warning { - param([string]$message) - Write-Host "⚠️ $message" -ForegroundColor DarkYellow -} - -# Configuration - Can be overridden by environment variables -$registryUrl = if ($env:REGISTRY_URL) { $env:REGISTRY_URL } else { "image.registry.local:5001" } -$services = @( - @{ - Name = "cart" - Path = "..\cart-cna-microservice" - Tag = "latest" - }, - @{ - Name = "products" - Path = "..\products-cna-microservice" - Tag = "latest" - }, - @{ - Name = "search" - Path = "..\search-cna-microservice" - Tag = "latest" - }, - @{ - Name = "store-ui" - Path = "..\store-ui" - Tag = "latest" - }, - @{ - Name = "users" - Path = "..\users-cna-microservice" - Tag = "latest" - } -) - -# Check if registry is running -Write-Phase "Checking Local Registry" -$registryContainer = docker ps --filter "name=kind-registry" --format "{{.Names}}" -if (-not $registryContainer) { - Write-Warning "Local registry is not running! Please run setup-kind.ps1 first." - exit 1 -} -Write-Success "Local registry is running at $registryUrl" - -foreach ($service in $services) { - Write-Phase "Processing $($service.Name) Service" - - # Build the image - Write-Step "Building image for $($service.Name)..." - Write-Progress "Running docker build in $($service.Path)" - - Push-Location $service.Path - try { - docker build -t "$($service.Name):$($service.Tag)" . - if ($LASTEXITCODE -ne 0) { throw "Docker build failed" } - Write-Success "Built $($service.Name):$($service.Tag)" - - # Tag the image for local registry - Write-Step "Tagging image for local registry..." - $registryImage = "$registryUrl/$($service.Name):$($service.Tag)" - docker tag "$($service.Name):$($service.Tag)" $registryImage - Write-Success "Tagged as $registryImage" - - # Push to local registry - Write-Step "Pushing to local registry..." - docker push $registryImage - if ($LASTEXITCODE -ne 0) { throw "Docker push failed" } - Write-Success "Successfully pushed $registryImage" - - } catch { - Write-Warning "Failed to process $($service.Name): $_" - } finally { - Pop-Location - } -} - -Write-Phase "Build and Push Summary" -Write-Success "All services have been processed" -Write-Host "`nNext steps:" -Write-Host "1. You can verify images in the registry using: docker images" -Write-Host "2. To see pushed images: curl http://$registryUrl/v2/_catalog" diff --git a/infra/build-and-push-images.sh b/infra/build-and-push-images.sh deleted file mode 100755 index bb3531c..0000000 --- a/infra/build-and-push-images.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# Helper Functions -function write_phase() { - echo -e "\nπŸš€ $1" -} - -function write_step() { - echo -e "\nπŸ“‹ $1" -} - -function write_progress() { - echo -e " $1" -} - -function write_success() { - echo -e "βœ… $1" -} - -function write_warning() { - echo -e "⚠️ $1" -} - -# Configuration - Can be overridden by environment variables -registry_url=${REGISTRY_URL:-"image.registry.local:5001"} - -# Define services to build and push -declare -a services=( - "cart:../cart-cna-microservice:latest" - "products:../products-cna-microservice:latest" - "search:../search-cna-microservice:latest" - "store-ui:../store-ui:latest" - "users:../users-cna-microservice:latest" -) - -# Check if registry is running -write_phase "Checking Local Registry" -registry_container=$(docker ps --filter "name=kind-registry" --format "{{.Names}}") -if [[ -z "$registry_container" ]]; then - write_warning "Local registry is not running! Please run setup-kind.sh first." - exit 1 -fi -write_success "Local registry is running at $registry_url" - -# Process each service -for service_info in "${services[@]}"; do - # Parse service information - IFS=: read -r name path tag <<< "$service_info" - - write_phase "Processing $name Service" - - # Build the image - write_step "Building image for $name..." - write_progress "Running docker build in $path" - - pushd "$path" > /dev/null - - if docker build -t "$name:$tag" .; then - write_success "Built $name:$tag" - - # Tag the image for local registry - write_step "Tagging image for local registry..." - registry_image="$registry_url/$name:$tag" - docker tag "$name:$tag" "$registry_image" - write_success "Tagged as $registry_image" - - # Push to local registry - write_step "Pushing to local registry..." - if docker push "$registry_image"; then - write_success "Successfully pushed $registry_image" - else - write_warning "Failed to push $name: Docker push failed" - fi - else - write_warning "Failed to build $name: Docker build failed" - fi - - popd > /dev/null -done - -write_phase "Build and Push Summary" -write_success "All services have been processed" -echo -e "\nNext steps:" -echo "1. You can verify images in the registry using: docker images" -echo "2. To see pushed images: curl http://$registry_url/v2/_catalog" \ No newline at end of file diff --git a/infra/create-dev-env.ps1 b/infra/create-dev-env.ps1 deleted file mode 100644 index 3977445..0000000 --- a/infra/create-dev-env.ps1 +++ /dev/null @@ -1,230 +0,0 @@ -[CmdletBinding()] -param( - [Parameter()] - [switch]$Reset, - - [Parameter()] - [switch]$Continue, - - [Parameter()] - [ValidateSet('prerequisites', 'cluster', 'images', 'infrastructure', 'applications')] - [string]$Step -) - -# Helper Functions - Importing common styling functions -. "$PSScriptRoot\build-and-push-images.ps1" - -# State management -$stateFile = Join-Path $PSScriptRoot ".dev-env-state" - -function Get-SetupState { - if (Test-Path $stateFile) { - Get-Content $stateFile - } else { - "init" - } -} - -function Update-SetupState { - param([string]$state) - $state | Set-Content $stateFile -} - -function Reset-SetupState { - if (Test-Path $stateFile) { - Remove-Item $stateFile - } -} - -function Test-Command { - param([string]$command) - - try { - Get-Command $command -ErrorAction Stop | Out-Null - return $true - } catch { - return $false - } -} - -function Test-Prerequisites { - Write-Phase "Checking Prerequisites" - - $prerequisites = @{ - "docker" = "Docker" - "kind" = "KinD (Kubernetes in Docker)" - "kubectl" = "kubectl" - } - - $allPresent = $true - foreach ($prereq in $prerequisites.GetEnumerator()) { - if (Test-Command $prereq.Key) { - Write-Success "$($prereq.Value) is installed" - } else { - Write-Warning "$($prereq.Value) is not installed!" - $allPresent = $false - } - } - - if (-not $allPresent) { - Write-Warning "Please install missing prerequisites and try again" - exit 1 - } -} - -function Initialize-DevCluster { - Write-Phase "Setting up KinD Cluster" - - # Run the setup-kind script - & "$PSScriptRoot\setup-kind.ps1" - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to setup KinD cluster" - exit 1 - } -} - -function Build-PushImages { - Write-Phase "Building and Pushing Images" - - # Run the build and push script - & "$PSScriptRoot\build-and-push-images.ps1" - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to build and push images" - exit 1 - } -} - -function Deploy-Infrastructure { - Write-Phase "Deploying Shared Infrastructure" - - Write-Step "Deploying shared services (Redis, MongoDB, Elasticsearch)" - kubectl apply -k "$PSScriptRoot\k8s\shared-services\overlays\local" - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to deploy shared services" - exit 1 - } - Write-Success "Shared services deployed successfully" - - # Wait for shared services to be ready - Write-Progress "Waiting for shared services to be ready..." - Start-Sleep -Seconds 10 - kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=shared-services -n shared-services --timeout=120s -} - -function Deploy-Applications { - Write-Phase "Deploying Application Services" - - Write-Step "Deploying application services" - kubectl apply -k "$PSScriptRoot\k8s\apps\overlays\local" - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to deploy application services" - exit 1 - } - Write-Success "Application services deployed successfully" - - # Wait for applications to be ready - Write-Progress "Waiting for application services to be ready..." - Start-Sleep -Seconds 10 - kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=application -n e-commerce --timeout=180s -} - -function Show-Environment-Info { - Write-Phase "Environment Information" - - Write-Host "`nAccess your applications:" - Write-Host "Store UI: http://localhost:80" - Write-Host "API Endpoints:" - Write-Host " - Cart Service: http://localhost:8080" - Write-Host " - Products Service: http://localhost:8081" - Write-Host " - Search Service: http://localhost:8082" - Write-Host " - Users Service: http://localhost:8083" - - Write-Host "`nUseful commands:" - Write-Host " kubectl get pods -A # List all pods" - Write-Host " kubectl get services -A # List all services" - Write-Host " kubectl logs -f # Follow pod logs" - Write-Host "`nTo clean up the environment:" - Write-Host " kind delete cluster --name e-commerce-cluster" -} - -# Main execution flow -try { - Write-Host "πŸš€ Creating Development Environment" -ForegroundColor Cyan - - # Get current state - $currentState = Get-SetupState - - if ($Reset) { - Reset-SetupState - $currentState = "init" - Write-Host "Reset setup state. Starting fresh." -ForegroundColor Yellow - } - - # Define the steps - $steps = @( - @{ - Name = "prerequisites" - Action = { Test-Prerequisites } - Desc = "Checking prerequisites" - }, - @{ - Name = "cluster" - Action = { Initialize-DevCluster } - Desc = "Setting up KinD cluster" - }, - @{ - Name = "images" - Action = { Build-PushImages } - Desc = "Building and pushing images" - }, - @{ - Name = "infrastructure" - Action = { Deploy-Infrastructure } - Desc = "Deploying infrastructure" - }, - @{ - Name = "applications" - Action = { Deploy-Applications } - Desc = "Deploying applications" - } - ) - - # Determine start index - $startIndex = 0 - - if ($PSBoundParameters.ContainsKey('Step')) { - $stepNames = $steps | ForEach-Object { $_.Name } - $startIndex = [array]::IndexOf($stepNames, $Step) - if ($startIndex -ge 0) { - Write-Host "Starting from $($steps[$startIndex].Desc)" -ForegroundColor Yellow - } - } elseif ($Continue -and $currentState -ne "init") { - $stepNames = $steps | ForEach-Object { $_.Name } - $continueIndex = [array]::IndexOf($stepNames, $currentState) - if ($continueIndex -ge 0) { - $startIndex = $continueIndex + 1 - if ($startIndex -lt $steps.Count) { - Write-Host "Continuing from $($steps[$startIndex].Desc)" -ForegroundColor Yellow - } - } - } - - # Execute steps - for ($i = $startIndex; $i -lt $steps.Count; $i++) { - $step = $steps[$i] - & $step.Action - Update-SetupState $step.Name - } - - Show-Environment-Info - Update-SetupState "complete" - Write-Success "Development environment is ready!" - -} catch { - Write-Warning "Failed to create development environment: $_" - Write-Host "`nTo continue from this point later, run:" - Write-Host " .\create-dev-env.ps1 -Continue" - Write-Host "To start fresh:" - Write-Host " .\create-dev-env.ps1 -Reset" - exit 1 -} diff --git a/infra/create-dev-env.sh b/infra/create-dev-env.sh deleted file mode 100644 index ded2426..0000000 --- a/infra/create-dev-env.sh +++ /dev/null @@ -1,281 +0,0 @@ -#!/bin/bash -# Bash equivalent of create-dev-env.ps1 - -# Command line arguments handling -RESET=false -CONTINUE=false -STEP="" - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -r|--reset) - RESET=true - shift - ;; - -c|--continue) - CONTINUE=true - shift - ;; - -s|--step) - STEP="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" - echo "Usage: $0 [--reset] [--continue] [--step ]" - echo "Available steps: prerequisites, cluster, images, infrastructure, applications" - exit 1 - ;; - esac -done - -# Check if step is valid -if [[ -n "$STEP" ]]; then - valid_steps=("prerequisites" "cluster" "images" "infrastructure" "applications") - if [[ ! " ${valid_steps[@]} " =~ " ${STEP} " ]]; then - echo "Invalid step: $STEP" - echo "Available steps: ${valid_steps[*]}" - exit 1 - fi -fi - -# Helper Functions - Colors and formatting -CYAN='\033[0;36m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -RED='\033[0;31m' -GRAY='\033[0;37m' -NC='\033[0m' # No Color - -# Source common styling functions if available -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [[ -f "$SCRIPT_DIR/build-and-push-images.sh" ]]; then - source "$SCRIPT_DIR/build-and-push-images.sh" -fi - -# State management -STATE_FILE="$SCRIPT_DIR/.dev-env-state" - -function get_setup_state() { - if [[ -f "$STATE_FILE" ]]; then - cat "$STATE_FILE" - else - echo "init" - fi -} - -function update_setup_state() { - echo "$1" > "$STATE_FILE" -} - -function reset_setup_state() { - if [[ -f "$STATE_FILE" ]]; then - rm "$STATE_FILE" - fi -} - -function write_phase() { - echo -e "\n${CYAN}πŸš€ $1${NC}" -} - -function write_step() { - echo -e "\n${YELLOW}πŸ“‹ $1${NC}" -} - -function write_progress() { - echo -e " ${GRAY}$1${NC}" -} - -function write_success() { - echo -e "${GREEN}βœ… $1${NC}" -} - -function write_warning() { - echo -e "${RED}⚠️ $1${NC}" -} - -function test_command() { - if command -v "$1" &> /dev/null; then - return 0 - else - return 1 - fi -} - -function test_prerequisites() { - write_phase "Checking Prerequisites" - - prerequisites=("docker:Docker" "kind:KinD (Kubernetes in Docker)" "kubectl:kubectl") - - all_present=true - for prereq in "${prerequisites[@]}"; do - cmd="${prereq%%:*}" - name="${prereq#*:}" - - if test_command "$cmd"; then - write_success "$name is installed" - else - write_warning "$name is not installed!" - all_present=false - fi - done - - if [[ "$all_present" == "false" ]]; then - write_warning "Please install missing prerequisites and try again" - exit 1 - fi -} - -function initialize_dev_cluster() { - write_phase "Setting up KinD Cluster" - - # Run the setup-kind script - bash "$SCRIPT_DIR/setup-kind.sh" - if [[ $? -ne 0 ]]; then - write_warning "Failed to setup KinD cluster" - exit 1 - fi -} - -function build_push_images() { - write_phase "Building and Pushing Images" - - # Run the build and push script - bash "$SCRIPT_DIR/build-and-push-images.sh" - if [[ $? -ne 0 ]]; then - write_warning "Failed to build and push images" - exit 1 - fi -} - -function deploy_infrastructure() { - write_phase "Deploying Shared Infrastructure" - - write_step "Deploying shared services (Redis, MongoDB, Elasticsearch)" - kubectl apply -k "$SCRIPT_DIR/k8s/shared-services/overlays/local" - if [[ $? -ne 0 ]]; then - write_warning "Failed to deploy shared services" - exit 1 - fi - write_success "Shared services deployed successfully" - - # Wait for shared services to be ready - write_progress "Waiting for shared services to be ready..." - sleep 10 - kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=shared-services -n shared-services --timeout=120s -} - -function deploy_applications() { - write_phase "Deploying Application Services" - - write_step "Deploying application services" - kubectl apply -k "$SCRIPT_DIR/k8s/apps/overlays/local" - if [[ $? -ne 0 ]]; then - write_warning "Failed to deploy application services" - exit 1 - fi - write_success "Application services deployed successfully" - - # Wait for applications to be ready - write_progress "Waiting for application services to be ready..." - sleep 10 - kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=application -n e-commerce --timeout=180s -} - -function show_environment_info() { - write_phase "Environment Information" - - echo -e "\nAccess your applications:" - echo "Store UI: http://localhost:80" - echo "API Endpoints:" - echo " - Cart Service: http://localhost:8080" - echo " - Products Service: http://localhost:8081" - echo " - Search Service: http://localhost:8082" - echo " - Users Service: http://localhost:8083" - - echo -e "\nUseful commands:" - echo " kubectl get pods -A # List all pods" - echo " kubectl get services -A # List all services" - echo " kubectl logs -f # Follow pod logs" - echo -e "\nTo clean up the environment:" - echo " kind delete cluster --name e-commerce-cluster" -} - -# Main execution flow -try_main() { - echo -e "${CYAN}πŸš€ Creating Development Environment${NC}" - - # Get current state - current_state=$(get_setup_state) - - if [[ "$RESET" == "true" ]]; then - reset_setup_state - current_state="init" - echo -e "${YELLOW}Reset setup state. Starting fresh.${NC}" - fi - - # Define the steps - declare -a steps=( - "prerequisites:test_prerequisites:Checking prerequisites" - "cluster:initialize_dev_cluster:Setting up KinD cluster" - "images:build_push_images:Building and pushing images" - "infrastructure:deploy_infrastructure:Deploying infrastructure" - "applications:deploy_applications:Deploying applications" - ) - - # Determine start index - start_index=0 - - if [[ -n "$STEP" ]]; then - for i in "${!steps[@]}"; do - step_name="${steps[$i]%%:*}" - if [[ "$step_name" == "$STEP" ]]; then - start_index=$i - echo -e "${YELLOW}Starting from ${steps[$i]##*:}${NC}" - break - fi - done - elif [[ "$CONTINUE" == "true" && "$current_state" != "init" ]]; then - for i in "${!steps[@]}"; do - step_name="${steps[$i]%%:*}" - if [[ "$step_name" == "$current_state" ]]; then - start_index=$((i + 1)) - if [[ $start_index -lt ${#steps[@]} ]]; then - echo -e "${YELLOW}Continuing from ${steps[$start_index]##*:}${NC}" - fi - break - fi - done - fi - - # Execute steps - for ((i=start_index; i<${#steps[@]}; i++)); do - step="${steps[$i]}" - step_name="${step%%:*}" - step_function="${step#*:}" - step_function="${step_function%%:*}" - - # Execute the function - $step_function - - # Update state - update_setup_state "$step_name" - done - - show_environment_info - update_setup_state "complete" - write_success "Development environment is ready!" - - return 0 -} - -# Execute main function with error handling -try_main || { - write_warning "Failed to create development environment: $?" - echo -e "\nTo continue from this point later, run:" - echo " ./create-dev-env.sh --continue" - echo "To start fresh:" - echo " ./create-dev-env.sh --reset" - exit 1 -} \ No newline at end of file diff --git a/infra/k8s/apps/base/cart/deployment.yaml b/infra/k8s/apps/base/cart/deployment.yaml index f763a25..025ca7b 100644 --- a/infra/k8s/apps/base/cart/deployment.yaml +++ b/infra/k8s/apps/base/cart/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: cart-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: cart diff --git a/infra/k8s/apps/base/cart/kustomization.yaml b/infra/k8s/apps/base/cart/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/cart/kustomization.yaml +++ b/infra/k8s/apps/base/cart/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/products/deployment.yaml b/infra/k8s/apps/base/products/deployment.yaml index c738770..3c95d1d 100644 --- a/infra/k8s/apps/base/products/deployment.yaml +++ b/infra/k8s/apps/base/products/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: products-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: products diff --git a/infra/k8s/apps/base/products/kustomization.yaml b/infra/k8s/apps/base/products/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/products/kustomization.yaml +++ b/infra/k8s/apps/base/products/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/search/deployment.yaml b/infra/k8s/apps/base/search/deployment.yaml index 7f5c434..b816626 100644 --- a/infra/k8s/apps/base/search/deployment.yaml +++ b/infra/k8s/apps/base/search/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: search-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: search diff --git a/infra/k8s/apps/base/search/kustomization.yaml b/infra/k8s/apps/base/search/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/search/kustomization.yaml +++ b/infra/k8s/apps/base/search/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/store-ui/deployment.yaml b/infra/k8s/apps/base/store-ui/deployment.yaml index 40a3800..b740f0e 100644 --- a/infra/k8s/apps/base/store-ui/deployment.yaml +++ b/infra/k8s/apps/base/store-ui/deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: store-ui-deployment + spec: selector: matchLabels: @@ -10,6 +11,11 @@ spec: metadata: labels: app: store-ui-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: store-ui diff --git a/infra/k8s/apps/base/store-ui/kustomization.yaml b/infra/k8s/apps/base/store-ui/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/store-ui/kustomization.yaml +++ b/infra/k8s/apps/base/store-ui/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/users/deployment.yaml b/infra/k8s/apps/base/users/deployment.yaml index 1a64298..2c10f0c 100644 --- a/infra/k8s/apps/base/users/deployment.yaml +++ b/infra/k8s/apps/base/users/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: users-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: users diff --git a/infra/k8s/apps/base/users/kustomization.yaml b/infra/k8s/apps/base/users/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/users/kustomization.yaml +++ b/infra/k8s/apps/base/users/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml new file mode 100644 index 0000000..0d8c483 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: cart + namespace: NAMESPACE_PLACEHOLDER +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/cart + rewrite: + uri: / + route: + - destination: + host: cart-service.NAMESPACE_PLACEHOLDER.svc.cluster.local + port: + number: 7000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml new file mode 100644 index 0000000..f940e3e --- /dev/null +++ b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- ../../../base/cart +- cart-vs.yaml + +images: +- name: cart:latest + newName: kubecondemo.azurecr.io/cart + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/kustomization.yaml b/infra/k8s/apps/overlays/azure/kustomization.yaml new file mode 100644 index 0000000..846895f --- /dev/null +++ b/infra/k8s/apps/overlays/azure/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- store-ui +- cart +- products +- search +- users \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/products/kustomization.yaml b/infra/k8s/apps/overlays/azure/products/kustomization.yaml new file mode 100644 index 0000000..e577302 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/products/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- ../../../base/products +- products-vs.yaml + +images: +- name: products:latest + newName: kubecondemo.azurecr.io/products + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/products/products-vs.yaml b/infra/k8s/apps/overlays/azure/products/products-vs.yaml new file mode 100644 index 0000000..b2d745c --- /dev/null +++ b/infra/k8s/apps/overlays/azure/products/products-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: products + namespace: NAMESPACE_PLACEHOLDER +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/products + rewrite: + uri: / + route: + - destination: + host: products-service.NAMESPACE_PLACEHOLDER.svc.cluster.local + port: + number: 5000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/search/kustomization.yaml b/infra/k8s/apps/overlays/azure/search/kustomization.yaml new file mode 100644 index 0000000..378aab2 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/search/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- ../../../base/search +- search-vs.yaml + +images: +- name: search:latest + newName: kubecondemo.azurecr.io/search + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/search/search-vs.yaml b/infra/k8s/apps/overlays/azure/search/search-vs.yaml new file mode 100644 index 0000000..e4fffa8 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/search/search-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: search + namespace: NAMESPACE_PLACEHOLDER +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/search + rewrite: + uri: / + route: + - destination: + host: search-service.NAMESPACE_PLACEHOLDER.svc.cluster.local + port: + number: 4000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml new file mode 100644 index 0000000..8f72f79 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml @@ -0,0 +1,25 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- ../../../base/store-ui +- store-ui-gateway.yaml +- store-ui-vs.yaml + +images: +- name: store-ui:latest + newName: kubecondemo.azurecr.io/store-ui + newTag: latest + +configMapGenerator: +- behavior: create + literals: + # Use the Istio Gateway URL for browser-based API calls + - REACT_APP_PRODUCTS_URL_BASE=/api/products/ + - REACT_APP_CART_URL_BASE=/api/cart/ + - REACT_APP_SEARCH_URL_BASE=/api/search/ + - REACT_APP_USERS_URL_BASE=/api/users/ + name: store-ui-configmap +generatorOptions: + disableNameSuffixHash: true \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml b/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml new file mode 100644 index 0000000..1db6c9d --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml @@ -0,0 +1,15 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: store-ui-gateway + namespace: aks-istio-ingress +spec: + selector: + istio: aks-istio-ingressgateway-external + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "*" \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml new file mode 100644 index 0000000..3480e0c --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: store-ui + namespace: NAMESPACE_PLACEHOLDER +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway # Fixed namespace for the shared gateway + http: + - match: # everything that hits "/" + - uri: + prefix: / + route: + - destination: + host: store-ui-service.NAMESPACE_PLACEHOLDER.svc.cluster.local + port: + number: 80 # servicePort in the Service object \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/users/kustomization.yaml b/infra/k8s/apps/overlays/azure/users/kustomization.yaml new file mode 100644 index 0000000..2c2e699 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/users/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: NAMESPACE_PLACEHOLDER + +resources: +- ../../../base/users +- users-vs.yaml + +images: +- name: users:latest + newName: kubecondemo.azurecr.io/users + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/users/users-vs.yaml b/infra/k8s/apps/overlays/azure/users/users-vs.yaml new file mode 100644 index 0000000..65d2b26 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/users/users-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: users + namespace: NAMESPACE_PLACEHOLDER +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/users + rewrite: + uri: / + route: + - destination: + host: users-service.NAMESPACE_PLACEHOLDER.svc.cluster.local + port: + number: 9090 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/local/cart/deployment-patch.yaml b/infra/k8s/apps/overlays/local/cart/deployment-patch.yaml index 9c3df73..77ca455 100644 --- a/infra/k8s/apps/overlays/local/cart/deployment-patch.yaml +++ b/infra/k8s/apps/overlays/local/cart/deployment-patch.yaml @@ -3,7 +3,13 @@ kind: Deployment metadata: name: cart-deployment spec: + selector: + matchLabels: + app: cart-deployment template: + metadata: + labels: + app: cart-deployment spec: containers: - name: cart diff --git a/infra/k8s/apps/overlays/local/cart/kustomization.yaml b/infra/k8s/apps/overlays/local/cart/kustomization.yaml index b56bc55..42341b4 100644 --- a/infra/k8s/apps/overlays/local/cart/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/cart/kustomization.yaml @@ -1,10 +1,15 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/cart - service-nodeport.yaml +images: +- name: cart:latest + newName: image.registry.local:5001/cart + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/products/deployment-patch.yaml b/infra/k8s/apps/overlays/local/products/deployment-patch.yaml index 0f0afd9..e98fb54 100644 --- a/infra/k8s/apps/overlays/local/products/deployment-patch.yaml +++ b/infra/k8s/apps/overlays/local/products/deployment-patch.yaml @@ -3,7 +3,13 @@ kind: Deployment metadata: name: products-deployment spec: + selector: + matchLabels: + app: products-deployment template: + metadata: + labels: + app: products-deployment spec: containers: - name: products diff --git a/infra/k8s/apps/overlays/local/products/kustomization.yaml b/infra/k8s/apps/overlays/local/products/kustomization.yaml index f59297c..82acbbb 100644 --- a/infra/k8s/apps/overlays/local/products/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/products/kustomization.yaml @@ -1,14 +1,20 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/products - service-nodeport.yaml + +images: +- name: products:latest + newName: image.registry.local:5001/products + newTag: latest + configMapGenerator: - behavior: create literals: - MONGO_URI=mongodb://mongo-root-username:mongo-root-password@mongo-service.shared-services.svc.cluster.local:27017/ - - DATABASE=e-commerce + - DATABASE=NAMESPACE_PLACEHOLDER name: products-configmap generatorOptions: disableNameSuffixHash: true diff --git a/infra/k8s/apps/overlays/local/search/deployment-patch.yaml b/infra/k8s/apps/overlays/local/search/deployment-patch.yaml index b32282d..4abe0d6 100644 --- a/infra/k8s/apps/overlays/local/search/deployment-patch.yaml +++ b/infra/k8s/apps/overlays/local/search/deployment-patch.yaml @@ -3,7 +3,13 @@ kind: Deployment metadata: name: search-deployment spec: + selector: + matchLabels: + app: search-deployment template: + metadata: + labels: + app: search-deployment spec: containers: - name: search diff --git a/infra/k8s/apps/overlays/local/search/kustomization.yaml b/infra/k8s/apps/overlays/local/search/kustomization.yaml index 38f0ff3..de19898 100644 --- a/infra/k8s/apps/overlays/local/search/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/search/kustomization.yaml @@ -1,9 +1,15 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/search - service-nodeport.yaml + +images: +- name: search:latest + newName: image.registry.local:5001/search + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/store-ui/deployment-patch.yaml b/infra/k8s/apps/overlays/local/store-ui/deployment-patch.yaml index 344688d..f3cc010 100644 --- a/infra/k8s/apps/overlays/local/store-ui/deployment-patch.yaml +++ b/infra/k8s/apps/overlays/local/store-ui/deployment-patch.yaml @@ -3,7 +3,13 @@ kind: Deployment metadata: name: store-ui-deployment spec: + selector: + matchLabels: + app: store-ui-deployment template: + metadata: + labels: + app: store-ui-deployment spec: containers: - name: store-ui diff --git a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml index d459b3d..31c32be 100644 --- a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml @@ -1,11 +1,16 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/store-ui - service-nodeport.yaml +images: +- name: store-ui:latest + newName: image.registry.local:5001/store-ui + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/users/deployment-patch.yaml b/infra/k8s/apps/overlays/local/users/deployment-patch.yaml index 506a383..15b3aeb 100644 --- a/infra/k8s/apps/overlays/local/users/deployment-patch.yaml +++ b/infra/k8s/apps/overlays/local/users/deployment-patch.yaml @@ -3,7 +3,13 @@ kind: Deployment metadata: name: users-deployment spec: + selector: + matchLabels: + app: users-deployment template: + metadata: + labels: + app: users-deployment spec: containers: - name: users diff --git a/infra/k8s/apps/overlays/local/users/kustomization.yaml b/infra/k8s/apps/overlays/local/users/kustomization.yaml index 9d95475..1968f53 100644 --- a/infra/k8s/apps/overlays/local/users/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/users/kustomization.yaml @@ -1,14 +1,20 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/users - service-nodeport.yaml + +images: +- name: users:latest + newName: image.registry.local:5001/users + newTag: latest + configMapGenerator: - behavior: create literals: - MONGO_URI=mongodb://mongo-root-username:mongo-root-password@mongo-service.shared-services.svc.cluster.local:27017/ - - DATABASE=e-commerce + - DATABASE=NAMESPACE_PLACEHOLDER name: users-configmap generatorOptions: disableNameSuffixHash: true diff --git a/infra/setup-kind.ps1 b/infra/setup-kind.ps1 deleted file mode 100644 index 7fa7287..0000000 --- a/infra/setup-kind.ps1 +++ /dev/null @@ -1,397 +0,0 @@ -# Requires: Docker, Kind, kubectl installed on Windows - -#region Configuration and Helper Functions -$ErrorActionPreference = 'Stop' - -# Configuration -$clusterName = Read-Host "πŸ“› Enter a cluster name" -$regName = "kind-registry" -$regPort = "5001" # Explicitly defined as string -$kindConfigPath = "./kind-cluster-config.yaml" - -# Helper Functions -function Write-Phase { - param($phase) - Write-Host "`nπŸ“‹ Phase: $phase`n" -ForegroundColor Cyan -} - -function Write-Step { - param($step) - Write-Host " $step" -ForegroundColor Yellow -} - -function Write-Progress { - param($message) - Write-Host " β†’ $message" -ForegroundColor DarkGray -} - -function Write-Success { - param($message) - Write-Host " βœ“ $message" -ForegroundColor Green -} - -function Write-Warning { - param($message) - Write-Host " ⚠ $message" -ForegroundColor Yellow -} - -function Write-PhaseSummary { - param( - [string]$phase, - [string[]]$completedSteps - ) - Write-Host "`nπŸ“ Phase Summary: $phase" -ForegroundColor Magenta - foreach ($step in $completedSteps) { - Write-Host " βœ“ $step" -ForegroundColor Green - } - Write-Host "" -} - -function Start-Timer { - return [System.Diagnostics.Stopwatch]::StartNew() -} - -function Stop-Timer($stopwatch, $taskName) { - $stopwatch.Stop() - $elapsed = $stopwatch.Elapsed - - # Format time in a user-friendly way - $formattedTime = if ($elapsed.TotalMinutes -ge 1) { - "{0:0.0} minutes" -f $elapsed.TotalMinutes - } elseif ($elapsed.TotalSeconds -ge 1) { - "{0:0.0} seconds" -f $elapsed.TotalSeconds - } else { - "{0:0} milliseconds" -f $elapsed.TotalMilliseconds - } - - Write-Host "⏱️ $taskName completed in $formattedTime`n" -ForegroundColor Gray -} - -#region Pre-flight Checks -Write-Phase "Pre-flight Checks" - -Write-Step "Validating configuration..." -$timer = Start-Timer - -if ([string]::IsNullOrEmpty($regPort)) { - Write-Error "Registry port cannot be empty" - exit 1 -} - -if (-not (Test-Path $kindConfigPath)) { - Write-Error "Kind cluster config file not found at $kindConfigPath" - exit 1 -} - -Write-Step "Checking required tools..." -$tools = @( - @{ Name = "Docker"; Command = "docker --version" }, - @{ Name = "Kind"; Command = "kind --version" }, - @{ Name = "kubectl"; Command = "kubectl version --client" } -) - -foreach ($tool in $tools) { - try { - $null = Invoke-Expression $tool.Command - Write-Host " β†’ $($tool.Name) is installed" - } catch { - Write-Error "$($tool.Name) is not installed or not in PATH" - exit 1 - } -} - -Stop-Timer $timer "Pre-flight validation" - -Write-PhaseSummary "Pre-flight Checks" @( - "Configuration validated" - "Required files checked" - "Required tools verified" -) -#endregion - -#region Phase 1: Local Registry Setup -Write-Phase "Setting up Local Registry" - -Write-Step "Checking for existing registry container..." -$timer = Start-Timer -$regExists = docker ps -a --format "{{.Names}}" | Select-String -Pattern "^$regName$" -if (-not $regExists) { - Write-Step "Setting up new registry container..." - Write-Progress "Pulling registry:2 image..." - try { - # Pull the registry image with progress - $pullOutput = docker pull registry:2 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Success "Registry image pulled successfully" - } else { - Write-Error "Failed to pull registry image: $pullOutput" - exit 1 - } - - Write-Progress "Starting registry container on port ${regPort}..." - $containerId = docker run -d --restart=always -p "${regPort}:5000" --name $regName registry:2 - if ($containerId) { - Write-Progress "Waiting for registry to be ready..." - Start-Sleep -Seconds 2 - $health = docker inspect --format='{{.State.Status}}' $regName - Write-Progress "Registry status: $health" - if ($health -eq "running") { - Write-Host "βœ… Registry container successfully created and running" - } else { - Write-Error "Registry container is not running. Status: $health" - exit 1 - } - } - } catch { - Write-Error ("Failed to create registry container: {0}" -f $_.Exception.Message) - exit 1 - } -} else { - Write-Host "βœ… Registry already exists, checking status..." - $health = docker inspect --format='{{.State.Status}}' $regName - Write-Host " β†’ Registry status: $health" - if ($health -ne "running") { - Write-Host " β†’ Starting existing registry container..." - docker start $regName - } -} -Stop-Timer $timer "Registry setup" - -Write-PhaseSummary "Registry Setup" @( - "Registry container status checked" - "Registry running on port $regPort" - "Container health verified" -) -#endregion - -#region Phase 2: Cluster Creation -Write-Phase "Creating Kind Cluster" - -Write-Step "Validating cluster configuration..." -$timer = Start-Timer -Stop-Timer $timer "Config validation" - -Write-Step "Checking cluster status..." -$timer = Start-Timer -$existingClusters = kind get clusters - -# Clean up any existing cluster resources -Write-Step "Cleaning up any existing resources..." - -if ($existingClusters -contains $clusterName) { - Write-Progress "Deleting existing Kind cluster..." - kind delete cluster --name $clusterName -} - -# Clean up all related containers even if cluster delete failed -Write-Progress "Checking for leftover containers..." -$containerPatterns = @( - "$clusterName-control-plane", - "$clusterName-worker", - "kind-$clusterName-*" -) - -foreach ($pattern in $containerPatterns) { - $stuckContainers = docker ps -a --filter "name=$pattern" --format "{{.Names}}" - if ($stuckContainers) { - foreach ($container in $stuckContainers) { - Write-Progress "Removing container: $container" - docker rm -f $container 2>$null - } - } -} - -# Clean up any Kind networks if needed -$kindNetwork = docker network ls --filter "name=kind" --format "{{.Name}}" -if ($kindNetwork) { - Write-Progress "Removing old Kind network..." - docker network rm kind 2>$null -} - -# Give some time for resources to be cleaned up -Write-Progress "Waiting for cleanup to complete..." -Start-Sleep -Seconds 5 - -Write-Step "Creating Kubernetes cluster..." - -try { - Write-Host "`n" # Add some spacing for better readability - # Execute Kind directly to show its beautiful progress output - kind create cluster --name $clusterName --config $kindConfigPath - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to create cluster" - exit 1 - } - - Write-Host "`n" # Add spacing after Kind output - Write-Success "Cluster created successfully" - - # Verify cluster access - Write-Progress "Verifying cluster access..." - $nodes = kubectl get nodes -o wide 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Success "Cluster is accessible" - Write-Host "`nCluster Nodes:" - Write-Host $nodes -ForegroundColor DarkGray - } else { - Write-Warning "Cluster created but might not be fully ready yet" - } -} catch { - Write-Error ("Failed to create cluster: {0}" -f $_.Exception.Message) - exit 1 -} -Stop-Timer $timer "Cluster check/creation" - -Write-PhaseSummary "Cluster Creation" @( - "Old resources cleaned up" - "New cluster created: $clusterName" - "Configuration applied from: $kindConfigPath" -) -#endregion - -#region Phase 3: Registry Integration -Write-Phase "Configuring Registry Access" - -Write-Step "Setting up registry access in cluster nodes..." -$timer = Start-Timer -$regHostDir = "/etc/containerd/certs.d/image.registry.local:$regPort" - -# Verify cluster is ready before proceeding -Write-Host " β†’ Waiting for cluster to be ready..." -$retryCount = 0 -$maxRetries = 30 -do { - $kindNodes = kind get nodes --name $clusterName 2>$null - if ($kindNodes) { - Write-Host " β†’ Found cluster nodes: $($kindNodes.Count) node(s)" - break - } - $retryCount++ - if ($retryCount -eq $maxRetries) { - Write-Error "Timeout waiting for cluster nodes to be ready" - exit 1 - } - Write-Host " β†’ Waiting for nodes to be ready... (Attempt $retryCount/$maxRetries)" - Start-Sleep -Seconds 2 -} while ($true) - -$nodeCount = ($kindNodes | Measure-Object -Line).Lines -Write-Progress "Configuring $nodeCount cluster nodes..." - -foreach ($node in $kindNodes) { - Write-Progress "Setting up registry access on node: $node" - try { - docker exec $node mkdir -p $regHostDir - $hostsToml = "[host.`"http://$regName:5000`"]" - $hostsToml | docker exec -i $node /bin/sh -c "cat > $regHostDir/hosts.toml" - Write-Success "Node $node configured successfully" - } catch { - Write-Error ("Failed to configure node {0}: {1}" -f $node, $_.Exception.Message) - exit 1 - } -} -Stop-Timer $timer "Registry config to Kind nodes" - -Write-Step "Configuring network connectivity..." -$timer = Start-Timer -$connected = docker inspect -f='{{json .NetworkSettings.Networks.kind}}' $regName 2>$null -if ($connected -eq "null") { - docker network connect "kind" $regName - Write-Host "βœ… Connected registry to kind network." -} else { - Write-Host "πŸ”— Already connected." -} -Stop-Timer $timer "Network attachment" - -Write-PhaseSummary "Registry Integration" @( - "Registry configured on all nodes" - "Network connectivity established" - "Node configurations updated" -) -#endregion - -#region Phase 4: Kubernetes Configuration -Write-Phase "Configuring Kubernetes Settings" - -Write-Step "Creating registry configuration in Kubernetes..." -$timer = Start-Timer - -Write-Step "Waiting for Kubernetes API..." -Write-Host " β†’ Waiting for Kubernetes API to be ready..." -$retryCount = 0 -$maxRetries = 30 -do { - $apiStatus = kubectl get --raw='/readyz' 2>$null - if ($apiStatus -eq "ok") { - Write-Host " β†’ Kubernetes API is ready" - break - } - $retryCount++ - if ($retryCount -eq $maxRetries) { - Write-Error "Timeout waiting for Kubernetes API to be ready" - exit 1 - } - Write-Host " β†’ Waiting for API to be ready... (Attempt $retryCount/$maxRetries)" - Start-Sleep -Seconds 2 -} while ($true) - -# Create the ConfigMap -try { - $configMap = @" -apiVersion: v1 -kind: ConfigMap -metadata: - name: local-registry-hosting - namespace: kube-public -data: - localRegistryHosting.v1: | - host: "image.registry.local:$regPort" - help: "https://kind.sigs.k8s.io/docs/user/local-registry/" -"@ - - # Ensure kube-public namespace exists - kubectl create namespace kube-public --dry-run=client -o yaml | kubectl apply -f - - - # Apply the ConfigMap - $configMap | kubectl apply -f - --validate=true - Write-Host "βœ… Registry ConfigMap created successfully" -} catch { - Write-Error ("Failed to create registry ConfigMap: {0}" -f $_.Exception.Message) - exit 1 -} -Stop-Timer $timer "Registry documentation setup" - -Write-PhaseSummary "Kubernetes Configuration" @( - "Kubernetes API ready" - "Registry ConfigMap created" - "Namespace configuration complete" -) -#endregion - -#region Final Summary -Write-Phase "Setup Complete" - -Write-Host "✨ Your Kubernetes development environment is ready! ✨" -ForegroundColor Green -Write-Host @" - -πŸ”Έ Environment Details: - β€’ Cluster Name: $clusterName - β€’ Local Registry: image.registry.local:$regPort - β€’ Config Path: $kindConfigPath - -πŸ”Έ Verify Setup: - β€’ Check nodes: kubectl get nodes - β€’ Check system: kubectl get pods -A - β€’ Check registry: curl http://image.registry.local:$regPort/v2/_catalog - -πŸ”Έ Quick Start: - 1. Tag an image: docker tag myimage:latest image.registry.local:$regPort/myimage:latest - 2. Push to registry: docker push image.registry.local:$regPort/myimage:latest - 3. Deploy to K8s: kubectl apply -f your-deployment.yaml - -πŸ”Έ Documentation: - β€’ Kind: https://kind.sigs.k8s.io/docs/user/quick-start/ - β€’ Local Registry: https://kind.sigs.k8s.io/docs/user/local-registry/ - β€’ Kubernetes: https://kubernetes.io/docs/home/ -"@ -ForegroundColor Yellow -#endregion \ No newline at end of file diff --git a/infra/setup-kind.sh b/infra/setup-kind.sh deleted file mode 100755 index 5567096..0000000 --- a/infra/setup-kind.sh +++ /dev/null @@ -1,375 +0,0 @@ -#!/bin/bash -# Requires: Docker, Kind, kubectl installed on Linux - -# Exit on any error -set -e - -# Configuration -echo -e "πŸ“› Enter a cluster name: \c" -read cluster_name -reg_name="kind-registry" -reg_port="5001" # Explicitly defined as string -kind_config_path="./kind-cluster-config.yaml" - -# Helper Functions -function write_phase() { - echo -e "\nπŸ“‹ Phase: $1\n" -} - -function write_step() { - echo -e " $1" -} - -function write_progress() { - echo -e " β†’ $1" -} - -function write_success() { - echo -e " βœ“ $1" -} - -function write_warning() { - echo -e " ⚠ $1" -} - -function write_phase_summary() { - local phase="$1" - shift - local completed_steps=("$@") - - echo -e "\nπŸ“ Phase Summary: $phase" - for step in "${completed_steps[@]}"; do - echo -e " βœ“ $step" - done - echo "" -} - -function start_timer() { - start_time=$(date +%s.%N) -} - -function stop_timer() { - local end_time=$(date +%s.%N) - local elapsed=$(echo "$end_time - $start_time" | bc) - local task_name="$1" - - # Format time in a user-friendly way - if (( $(echo "$elapsed >= 60" | bc -l) )); then - local formatted_time=$(echo "scale=1; $elapsed / 60" | bc) - echo -e "⏱️ $task_name completed in ${formatted_time} minutes\n" - elif (( $(echo "$elapsed >= 1" | bc -l) )); then - local formatted_time=$(echo "scale=1; $elapsed" | bc) - echo -e "⏱️ $task_name completed in ${formatted_time} seconds\n" - else - local formatted_time=$(echo "scale=0; $elapsed * 1000" | bc) - echo -e "⏱️ $task_name completed in ${formatted_time} milliseconds\n" - fi -} - -#region Pre-flight Checks -write_phase "Pre-flight Checks" - -write_step "Validating configuration..." -start_timer - -if [[ -z "$reg_port" ]]; then - echo "Error: Registry port cannot be empty" - exit 1 -fi - -if [[ ! -f "$kind_config_path" ]]; then - echo "Error: Kind cluster config file not found at $kind_config_path" - exit 1 -fi - -write_step "Checking required tools..." -tools=("docker:Docker" "kind:Kind" "kubectl:kubectl") - -for tool_info in "${tools[@]}"; do - cmd="${tool_info%%:*}" - name="${tool_info#*:}" - - if command -v "$cmd" >/dev/null 2>&1; then - write_progress "$name is installed" - else - echo "Error: $name is not installed or not in PATH" - exit 1 - fi -done - -stop_timer "Pre-flight validation" - -write_phase_summary "Pre-flight Checks" \ - "Configuration validated" \ - "Required files checked" \ - "Required tools verified" -#endregion - -#region Phase 1: Local Registry Setup -write_phase "Setting up Local Registry" - -write_step "Checking for existing registry container..." -start_timer - -reg_exists=$(docker ps -a --format '{{.Names}}' | grep -E "^${reg_name}$" || true) -if [[ -z "$reg_exists" ]]; then - write_step "Setting up new registry container..." - write_progress "Pulling registry:2 image..." - - if ! docker pull registry:2; then - echo "Error: Failed to pull registry image" - exit 1 - fi - write_success "Registry image pulled successfully" - - write_progress "Starting registry container on port ${reg_port}..." - container_id=$(docker run -d --restart=always -p "${reg_port}:5000" --name "$reg_name" registry:2) - if [[ -n "$container_id" ]]; then - write_progress "Waiting for registry to be ready..." - sleep 2 - health=$(docker inspect --format='{{.State.Status}}' "$reg_name") - write_progress "Registry status: $health" - if [[ "$health" == "running" ]]; then - echo "βœ… Registry container successfully created and running" - else - echo "Error: Registry container is not running. Status: $health" - exit 1 - fi - fi -else - echo "βœ… Registry already exists, checking status..." - health=$(docker inspect --format='{{.State.Status}}' "$reg_name") - write_progress "Registry status: $health" - if [[ "$health" != "running" ]]; then - write_progress "Starting existing registry container..." - docker start "$reg_name" - fi -fi - -stop_timer "Registry setup" - -write_phase_summary "Registry Setup" \ - "Registry container status checked" \ - "Registry running on port $reg_port" \ - "Container health verified" -#endregion - -#region Phase 2: Cluster Creation -write_phase "Creating Kind Cluster" - -write_step "Validating cluster configuration..." -start_timer -stop_timer "Config validation" - -write_step "Checking cluster status..." -start_timer -existing_clusters=$(kind get clusters) - -# Clean up any existing cluster resources -write_step "Cleaning up any existing resources..." - -if echo "$existing_clusters" | grep -q "^${cluster_name}$"; then - write_progress "Deleting existing Kind cluster..." - kind delete cluster --name "$cluster_name" -fi - -# Clean up all related containers even if cluster delete failed -write_progress "Checking for leftover containers..." -container_patterns=( - "${cluster_name}-control-plane" - "${cluster_name}-worker" - "kind-${cluster_name}-*" -) - -for pattern in "${container_patterns[@]}"; do - stuck_containers=$(docker ps -a --filter "name=$pattern" --format "{{.Names}}" || true) - if [[ -n "$stuck_containers" ]]; then - echo "$stuck_containers" | while read container; do - write_progress "Removing container: $container" - docker rm -f "$container" 2>/dev/null || true - done - fi -done - -# Clean up any Kind networks if needed -kind_network=$(docker network ls --filter "name=kind" --format "{{.Name}}" || true) -if [[ -n "$kind_network" ]]; then - write_progress "Removing old Kind network..." - docker network rm kind 2>/dev/null || true -fi - -# Give some time for resources to be cleaned up -write_progress "Waiting for cleanup to complete..." -sleep 5 - -write_step "Creating Kubernetes cluster..." - -echo # Add some spacing for better readability -# Execute Kind directly to show its beautiful progress output -if ! kind create cluster --name "$cluster_name" --config "$kind_config_path"; then - echo "Error: Failed to create cluster" - exit 1 -fi - -echo # Add spacing after Kind output -write_success "Cluster created successfully" - -# Verify cluster access -write_progress "Verifying cluster access..." -if nodes=$(kubectl get nodes -o wide 2>&1); then - write_success "Cluster is accessible" - echo -e "\nCluster Nodes:" - echo "$nodes" -else - write_warning "Cluster created but might not be fully ready yet" -fi - -stop_timer "Cluster check/creation" - -write_phase_summary "Cluster Creation" \ - "Old resources cleaned up" \ - "New cluster created: $cluster_name" \ - "Configuration applied from: $kind_config_path" -#endregion - -#region Phase 3: Registry Integration -write_phase "Configuring Registry Access" - -write_step "Setting up registry access in cluster nodes..." -start_timer -reg_host_dir="/etc/containerd/certs.d/image.registry.local:$reg_port" - -# Verify cluster is ready before proceeding -write_progress "Waiting for cluster to be ready..." -retry_count=0 -max_retries=30 -while true; do - kind_nodes=$(kind get nodes --name "$cluster_name" 2>/dev/null || true) - if [[ -n "$kind_nodes" ]]; then - node_count=$(echo "$kind_nodes" | wc -l) - write_progress "Found cluster nodes: $node_count node(s)" - break - fi - - retry_count=$((retry_count + 1)) - if [[ $retry_count -eq $max_retries ]]; then - echo "Error: Timeout waiting for cluster nodes to be ready" - exit 1 - fi - - write_progress "Waiting for nodes to be ready... (Attempt $retry_count/$max_retries)" - sleep 2 -done - -write_progress "Configuring $node_count cluster nodes..." - -echo "$kind_nodes" | while read node; do - write_progress "Setting up registry access on node: $node" - docker exec "$node" mkdir -p "$reg_host_dir" - hosts_toml="[host.\"http://$reg_name:5000\"]" - echo "$hosts_toml" | docker exec -i "$node" /bin/sh -c "cat > $reg_host_dir/hosts.toml" - write_success "Node $node configured successfully" -done - -stop_timer "Registry config to Kind nodes" - -write_step "Configuring network connectivity..." -start_timer -connected=$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "$reg_name" 2>/dev/null || echo "null") -if [[ "$connected" == "null" ]]; then - docker network connect "kind" "$reg_name" - echo "βœ… Connected registry to kind network." -else - echo "πŸ”— Already connected." -fi -stop_timer "Network attachment" - -write_phase_summary "Registry Integration" \ - "Registry configured on all nodes" \ - "Network connectivity established" \ - "Node configurations updated" -#endregion - -#region Phase 4: Kubernetes Configuration -write_phase "Configuring Kubernetes Settings" - -write_step "Creating registry configuration in Kubernetes..." -start_timer - -write_step "Waiting for Kubernetes API..." -write_progress "Waiting for Kubernetes API to be ready..." -retry_count=0 -max_retries=30 -while true; do - api_status=$(kubectl get --raw='/readyz' 2>/dev/null || echo "") - if [[ "$api_status" == "ok" ]]; then - write_progress "Kubernetes API is ready" - break - fi - - retry_count=$((retry_count + 1)) - if [[ $retry_count -eq $max_retries ]]; then - echo "Error: Timeout waiting for Kubernetes API to be ready" - exit 1 - fi - - write_progress "Waiting for API to be ready... (Attempt $retry_count/$max_retries)" - sleep 2 -done - -# Create the ConfigMap -config_map=$(cat <=6.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -64,111 +80,571 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.16.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.17.12", - "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.17.12.tgz", - "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=4" + "node": ">=6.0.0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=0.8.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -257,28 +733,586 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@jsdevtools/ono": { + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", "hasInstallScript": true }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/node": { + "version": "24.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", + "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz", @@ -430,6 +1464,13 @@ "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", @@ -439,41 +1480,174 @@ "node": ">=8" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" } }, - "node_modules/bl": { - "version": "2.2.1", - "resolved": "https://registry.npmmirror.com/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", @@ -506,6 +1680,49 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/bson": { "version": "1.1.6", "resolved": "https://registry.npmmirror.com/bson/-/bson-1.1.6.tgz", @@ -514,6 +1731,13 @@ "node": ">=0.6.19" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -563,6 +1787,37 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -579,6 +1834,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", @@ -618,6 +1883,29 @@ "node": ">= 6" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", @@ -655,6 +1943,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -679,6 +2000,19 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", @@ -688,6 +2022,16 @@ "node": ">= 12" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -712,6 +2056,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -725,6 +2076,13 @@ "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", @@ -758,6 +2116,28 @@ "node": ">=10" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -780,12 +2160,47 @@ "ms": "2.0.0" } }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "1.5.1", "resolved": "https://registry.npmmirror.com/denque/-/denque-1.5.1.tgz", @@ -811,6 +2226,37 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", @@ -848,6 +2294,26 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/electron-to-chromium": { + "version": "1.5.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -910,6 +2376,32 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1066,6 +2558,20 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.4.0.tgz", @@ -1138,6 +2644,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -1201,6 +2733,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1242,9 +2791,23 @@ "node": ">= 0.8" } }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "dependencies": { @@ -1261,6 +2824,41 @@ "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", @@ -1310,6 +2908,26 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1339,6 +2957,16 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -1422,6 +3050,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -1442,6 +3077,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1453,6 +3104,13 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1534,150 +3192,902 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { - "node": ">=0.8.19" + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, "engines": { - "node": ">= 0.10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, + "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, + "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -1690,6 +4100,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -1708,6 +4131,39 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", @@ -1826,6 +4282,19 @@ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "dev": true }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1898,6 +4367,55 @@ "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2062,6 +4580,20 @@ "node": ">= 0.6" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", @@ -2249,6 +4781,51 @@ "node": ">= 0.8.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", @@ -2264,6 +4841,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -2302,6 +4889,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2319,6 +4916,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -2333,16 +4937,46 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "engines": { - "node": ">=8.6" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=8" } }, "node_modules/please-upgrade-node": { @@ -2378,11 +5012,53 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2410,6 +5086,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2446,6 +5139,13 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.7.tgz", @@ -2497,6 +5197,60 @@ "node": ">=4" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2506,6 +5260,16 @@ "node": ">=4" } }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -2772,6 +5536,23 @@ "semver": "bin/semver.js" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -2786,6 +5567,27 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -2795,6 +5597,36 @@ "memory-pager": "^1.0.2" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2825,6 +5657,20 @@ "node": ">=0.6.19" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", @@ -2865,6 +5711,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -2886,6 +5742,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz", + "integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz", + "integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", @@ -2898,6 +5827,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/swagger-jsdoc": { "version": "6.2.8", "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", @@ -2986,6 +5928,21 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", @@ -2998,6 +5955,13 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3048,6 +6012,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", @@ -3078,6 +6052,13 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3086,6 +6067,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", @@ -3114,6 +6126,21 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validator": { "version": "13.15.15", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", @@ -3130,6 +6157,16 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -3176,6 +6213,37 @@ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", @@ -3185,6 +6253,48 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/products-cna-microservice/package.json b/products-cna-microservice/package.json index 1caaf6d..6050a66 100644 --- a/products-cna-microservice/package.json +++ b/products-cna-microservice/package.json @@ -4,7 +4,7 @@ "main": "index.js", "scripts": { "start": "nodemon server.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest", "lint": "eslint .", "format": "prettier --single-quote --check .", "prepare": "cd ../ && husky install server/.husky" @@ -20,9 +20,11 @@ "devDependencies": { "eslint": "^8.0.0", "husky": "^7.0.2", + "jest": "^29.7.0", "lint-staged": "^11.2.3", "nodemon": "^2.0.12", - "prettier": "^2.4.1" + "prettier": "^2.4.1", + "supertest": "^7.1.3" }, "husky": { "hooks": { diff --git a/products-cna-microservice/server.js b/products-cna-microservice/server.js index 00dc19d..02194e2 100644 --- a/products-cna-microservice/server.js +++ b/products-cna-microservice/server.js @@ -58,16 +58,22 @@ loadData = () => { }); } -// perform a database connection when the server starts -dbo.connectToServer(function (err) { - if (err) { - console.error(err); - process.exit(); - } +// Only connect to the database and start the server if this file is run directly +if (require.main === module) { + // perform a database connection when the server starts + dbo.connectToServer(function (err) { + if (err) { + console.error(err); + process.exit(); + } - // start the Express server - app.listen(PORT, () => { - console.log(`Server is running on port: ${PORT}`); - loadData() + // start the Express server + app.listen(PORT, () => { + console.log(`Server is running on port: ${PORT}`); + loadData() + }); }); -}); +} + +// Export the app for testing +module.exports = { app, loadData }; diff --git a/products-cna-microservice/tests/api.test.js b/products-cna-microservice/tests/api.test.js new file mode 100644 index 0000000..736c8e9 --- /dev/null +++ b/products-cna-microservice/tests/api.test.js @@ -0,0 +1,76 @@ +const request = require('supertest'); +const { app } = require('../server'); +const dbo = require('../db/conn'); + +// Mock the database connection +jest.mock('../db/conn', () => ({ + getDb: jest.fn(), + connectToServer: jest.fn(callback => callback()) +})); + +describe('Products API', () => { + // Setup mock database responses before tests + beforeAll(() => { + // Mock database for deals endpoint + const mockDealsCollection = { + find: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn((callback) => { + callback(null, [ + { id: '1', title: 'Test Deal', description: 'This is a test deal' } + ]); + }) + }; + + // Mock database for product by SKU endpoint + const mockProductsCollection = { + findOne: jest.fn((query, callback) => { + if (query['variants.sku'] === 'TEST-SKU-123') { + callback(null, { + id: 'product-1', + name: 'Test Product', + variants: [{ sku: 'TEST-SKU-123', price: 9.99 }] + }); + } else { + callback(null, null); + } + }) + }; + + // Set up mock implementation for getDb + dbo.getDb.mockImplementation(() => ({ + collection: (collectionName) => { + if (collectionName === 'deals') { + return mockDealsCollection; + } + if (collectionName === 'products') { + return mockProductsCollection; + } + return null; + } + })); + }); + + // Test the /deals endpoint + test('GET /deals should return deals list', async () => { + const response = await request(app).get('/deals'); + expect(response.statusCode).toBe(200); + expect(response.body).toBeInstanceOf(Array); + expect(response.body.length).toBe(1); + expect(response.body[0].title).toBe('Test Deal'); + }); + + // Test the /products/sku/:id endpoint with a valid SKU + test('GET /products/sku/:id with valid SKU should return a product', async () => { + const response = await request(app).get('/products/sku/TEST-SKU-123'); + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('name', 'Test Product'); + expect(response.body.variants[0].sku).toBe('TEST-SKU-123'); + }); + + // Basic health check test + test('API server should be healthy', async () => { + // This test just verifies that the app exists and can handle requests + expect(app).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/search-cna-microservice/devspace.yaml b/search-cna-microservice/devspace.yaml index 0ed3e5c..b9c0972 100644 --- a/search-cna-microservice/devspace.yaml +++ b/search-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: search -images: - search: - image: image.registry.local:5001/search - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../.devpilot-scripts/devspace-pipelines.yaml" -deployments: - search: - kubectl: - manifests: - - ../infra/k8s/apps/overlays/local/search/ - kustomize: true +vars: + SERVICE_NAME: + default: "search" + K8S_MANIFEST_PATH_LOCAL: + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/search/" + K8S_MANIFEST_PATH_AZURE: + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/search/" dev: search: @@ -34,20 +31,8 @@ dev: command: ./devspace_start.sh ports: - port: "4000" - open: - - url: http://localhost:4000 env: - name: NODE_ENV value: "development" - name: ELASTIC_URL value: "http://localhost:9200/" - -commands: - start: - command: devspace enter search -- npm start - build: - command: devspace enter search -- npm run build - test: - command: devspace enter search -- npm test - install: - command: devspace enter search -- npm install diff --git a/store-ui/.github/prompts/ui-ux-improvements.prompt.md b/store-ui/.github/prompts/ui-ux-improvements.prompt.md new file mode 100644 index 0000000..6908d0f --- /dev/null +++ b/store-ui/.github/prompts/ui-ux-improvements.prompt.md @@ -0,0 +1,281 @@ +# React E-commerce UI/UX Improvement Prompts for Copilot + +## Project Overview +This is a React + TypeScript e-commerce application using Material-UI (MUI) v5, React Router, and Context API for state management. The goal is to improve UI/UX, accessibility, performance, and conversion rates. + +## Priority P0: Critical Performance & Accessibility Fixes + +### 1. Header Navigation Accessibility +**Prompt**: "Fix accessibility issues in Header.tsx component. Add proper ARIA labels to hamburger menu button, implement keyboard navigation for mobile drawer, and ensure all interactive elements are keyboard accessible." + +```typescript +// Add these attributes to hamburger menu button: +aria-label="Main menu" +aria-controls="mobile-navigation-drawer" +aria-expanded={mobileMenuOpen} + +// Add keyboard event handlers and focus management +// Implement proper ARIA roles for navigation elements +``` + +### 2. Image Performance Optimization +**Prompt**: "Convert all product images to WebP format with lazy loading. Implement responsive images with srcset and sizes attributes. Add aspect-ratio CSS to prevent layout shifts." + +```typescript +// Create ImageOptimizer component with: +// - WebP format conversion +// - Lazy loading with IntersectionObserver +// - Responsive srcset generation +// - Aspect ratio preservation +``` + +### 3. Search Component Enhancement +**Prompt**: "Refactor SearchComponent.tsx to separate suggestion endpoint from search endpoint. Add keyboard navigation (up/down arrows) to suggestion dropdown. Implement aria-live regions for dynamic content updates." + +```typescript +// Separate API calls: +// - /api/suggest for autocomplete (lightweight) +// - /api/search for full results +// Add keyboard navigation with aria-activedescendant +// Implement role="listbox" and aria-live="polite" +``` + +## Priority P1: Core UX Improvements + +### 4. Product Page Visual Hierarchy +**Prompt**: "Redesign product page layout to emphasize price and CTA button. Replace horizontal tabs with sticky accordions. Add trust signals (return policy, shipping info) near purchase button." + +```typescript +// Visual hierarchy improvements: +// - Larger, contrasting price display +// - Prominent CTA button styling +// - Sticky accordion for product details +// - Trust badges component near purchase area +``` + +### 5. Cart Badge Visibility +**Prompt**: "Fix cart badge visibility on mobile devices. Ensure badge shows on all screen sizes with proper contrast and larger touch targets." + +```typescript +// Update cart badge with: +// - showZero prop for MUI Badge +// - Larger anchorOrigin values +// - Improved contrast ratios +// - Minimum touch target size (44px) +``` + +### 6. Dark Mode System Preference +**Prompt**: "Implement system preference detection for dark mode. Use useMediaQuery to detect prefers-color-scheme and set initial theme accordingly." + +```typescript +// Add to Layout.tsx: +const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); +// Set initial mode based on system preference +``` + +## Priority P2: Enhanced Features + +### 7. Checkout Flow Simplification +**Prompt**: "Consolidate checkout process into single page with inline validation. Add progress indicators and real-time cost breakdown. Implement guest checkout option." + +```typescript +// Single-page checkout with: +// - Inline form validation +// - Progressive disclosure +// - Cost breakdown component +// - Guest checkout flow +``` + +### 8. Search Zero-State Improvements +**Prompt**: "Add zero-state content to search component showing popular queries, recent searches, and category suggestions when search input is empty." + +```typescript +// Zero-state component with: +// - Popular search terms +// - Category quick links +// - Recent searches (session storage) +// - Trending products carousel +``` + +### 9. Tab State Persistence +**Prompt**: "Fix tab state persistence in header navigation. Store active tab index using useLocation and useEffect to maintain user's position after page refresh." + +```typescript +// Implement tab persistence: +// - Use useLocation to track current route +// - Map routes to tab indices +// - Restore tab state on component mount +``` + +## Priority P3: SEO & Performance + +### 10. SEO Meta Tags +**Prompt**: "Implement React Helmet for dynamic SEO tags. Add page-specific titles, descriptions, Open Graph, and Twitter Card meta tags to each route component." + +```typescript +// Add to each page component: +// - Dynamic title tags +// - Meta descriptions +// - Open Graph tags +// - Twitter Card tags +// - Structured data (JSON-LD) +``` + +### 11. Code Splitting +**Prompt**: "Implement code splitting using React.lazy and Suspense for admin, user, and product sections. Add loading fallbacks and error boundaries." + +```typescript +// Lazy load components: +const AdminDashboard = React.lazy(() => import('./pages/Admin')); +const UserPage = React.lazy(() => import('./pages/User/UserPage')); +// Add Suspense wrappers with loading states +``` + +### 12. API Response Caching +**Prompt**: "Implement API response caching for product data and search results. Add cache invalidation strategies and optimize API call frequency." + +```typescript +// Add caching layer: +// - React Query or SWR for data fetching +// - Cache invalidation strategies +// - Optimistic updates for cart operations +``` + +## Component-Specific Improvements + +### Header Component Fixes +```typescript +// Fix these specific issues in Header.tsx: +// 1. Add proper ARIA labels to all interactive elements +// 2. Implement keyboard navigation for mobile drawer +// 3. Fix search input focus management +// 4. Add skip-to-content link +// 5. Improve mobile menu accessibility +``` + +### SearchComponent Enhancements +```typescript +// Improve SearchComponent.tsx: +// 1. Debounce suggestions API calls +// 2. Add keyboard navigation to dropdown +// 3. Implement ARIA live regions +// 4. Add loading states and error handling +// 5. Optimize mobile search experience +``` + +### Cart Context Optimization +```typescript +// Optimize CartContext.tsx: +// 1. Add optimistic updates +// 2. Implement error handling +// 3. Add loading states +// 4. Memoize expensive calculations +// 5. Add cart persistence +``` + +## Performance Monitoring + +### Core Web Vitals Implementation +**Prompt**: "Add Core Web Vitals monitoring to track LCP, INP, and CLS. Implement performance budgets and monitoring alerts." + +```typescript +// Add performance monitoring: +// - Web Vitals library integration +// - Performance observer setup +// - Metrics reporting dashboard +// - Budget enforcement +``` + +### Bundle Analysis +**Prompt**: "Analyze bundle size and implement tree shaking. Remove unused MUI components and optimize imports for better performance." + +```typescript +// Optimize imports: +// - Use specific MUI component imports +// - Implement tree shaking +// - Analyze bundle with webpack-bundle-analyzer +// - Remove unused dependencies +``` + +## Testing & Validation + +### Accessibility Testing +**Prompt**: "Add accessibility tests using @testing-library/jest-dom and axe-core. Implement keyboard navigation tests and screen reader compatibility checks." + +```typescript +// Add accessibility test suite: +// - Keyboard navigation tests +// - ARIA label validation +// - Color contrast checking +// - Screen reader compatibility +``` + +### Performance Testing +**Prompt**: "Implement performance regression testing using Lighthouse CI. Add Core Web Vitals monitoring and performance budgets." + +```typescript +// Performance test setup: +// - Lighthouse CI configuration +// - Performance budget enforcement +// - Regression detection +// - Automated monitoring +``` + +## Implementation Guidelines + +### Development Workflow +1. **Start with P0 items** - Focus on critical accessibility and performance fixes +2. **Test incrementally** - Implement and test each improvement separately +3. **Mobile-first approach** - Ensure mobile experience is prioritized +4. **Accessibility validation** - Test with screen readers and keyboard navigation +5. **Performance monitoring** - Track Core Web Vitals throughout development + +### Code Quality Standards +- Use TypeScript strict mode +- Implement proper error boundaries +- Add comprehensive prop types +- Follow MUI theme standards +- Maintain consistent code formatting + +### Testing Strategy +- Unit tests for all components +- Integration tests for user flows +- Accessibility tests for all interactive elements +- Performance tests for critical paths +- Cross-browser compatibility testing + +## Deployment Checklist + +### Pre-deployment Validation +- [ ] All accessibility tests pass +- [ ] Core Web Vitals meet targets +- [ ] Mobile responsiveness verified +- [ ] Cross-browser compatibility confirmed +- [ ] SEO meta tags implemented +- [ ] Performance budgets met + +### Post-deployment Monitoring +- [ ] Analytics tracking active +- [ ] Error monitoring configured +- [ ] Performance monitoring enabled +- [ ] User feedback collection setup +- [ ] A/B testing framework ready + +--- + +## Quick Reference Commands + +### For Copilot Chat +- "Implement accessibility fixes for Header component" +- "Add lazy loading to product images" +- "Create responsive search component" +- "Optimize cart performance" +- "Add SEO meta tags to product pages" + +### For Development +- Run accessibility audit: `npm run test:a11y` +- Check performance: `npm run lighthouse` +- Analyze bundle: `npm run analyze` +- Run tests: `npm test` + +This prompt file provides structured guidance for implementing UI/UX improvements with specific technical details and priorities. \ No newline at end of file diff --git a/store-ui/devspace.yaml b/store-ui/devspace.yaml index 8b61fb6..8e33682 100644 --- a/store-ui/devspace.yaml +++ b/store-ui/devspace.yaml @@ -1,17 +1,16 @@ version: v2beta1 name: store-ui -images: - store-ui: - image: image.registry.local:5001/store-ui - dockerfile: ./Dockerfile +imports: + - path: "../.devpilot-scripts/devspace-pipelines.yaml" -deployments: - store-ui: - kubectl: - manifests: - - ../infra/k8s/apps/overlays/local/store-ui/ - kustomize: true +vars: + SERVICE_NAME: + default: "store-ui" + K8S_MANIFEST_PATH_LOCAL: + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/store-ui/" + K8S_MANIFEST_PATH_AZURE: + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/store-ui/" dev: store-ui: @@ -47,15 +46,4 @@ dev: - name: REACT_APP_SEARCH_URL_BASE value: "http://localhost:8082/" - name: REACT_APP_USERS_URL_BASE - value: "http://localhost:8083/" - - -commands: - start: - command: devspace enter store-ui -- npm start - build: - command: devspace enter store-ui -- npm run build - test: - command: devspace enter store-ui -- npm test - install: - command: devspace enter store-ui -- npm install + value: "http://localhost:8083/" \ No newline at end of file diff --git a/store-ui/package-lock.json b/store-ui/package-lock.json index 1dc815d..73b7e64 100644 --- a/store-ui/package-lock.json +++ b/store-ui/package-lock.json @@ -20,10 +20,12 @@ "@types/node": "^16.11.41", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-helmet-async": "^1.0.3", "axios": "^0.27.2", "framer-motion": "^12.19.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-helmet-async": "^2.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "typescript": "^4.7.4", @@ -4144,6 +4146,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet-async": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/react-helmet-async/-/react-helmet-async-1.0.3.tgz", + "integrity": "sha512-DqbSuZPSHiH1l3XI/y8LbhrAGNh+Bpc9QY4MsYRM1yD4+qhax8bN4DInUMpv/tNyIdjsa+1V8XXmbRx8W5dB0w==", + "deprecated": "This is a stub types definition. react-helmet-async provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "react-helmet-async": "*" + } + }, "node_modules/@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -9133,6 +9145,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -14270,6 +14291,26 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15107,6 +15148,12 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16289,7 +16336,8 @@ "node_modules/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" + "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", + "license": "Apache-2.0" }, "node_modules/webidl-conversions": { "version": "6.1.0", @@ -19955,6 +20003,14 @@ "@types/react": "*" } }, + "@types/react-helmet-async": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/react-helmet-async/-/react-helmet-async-1.0.3.tgz", + "integrity": "sha512-DqbSuZPSHiH1l3XI/y8LbhrAGNh+Bpc9QY4MsYRM1yD4+qhax8bN4DInUMpv/tNyIdjsa+1V8XXmbRx8W5dB0w==", + "requires": { + "react-helmet-async": "*" + } + }, "@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -23595,6 +23651,14 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -27142,6 +27206,21 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "requires": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -27759,6 +27838,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/store-ui/package.json b/store-ui/package.json index 98754a6..5c8fc0d 100644 --- a/store-ui/package.json +++ b/store-ui/package.json @@ -15,10 +15,12 @@ "@types/node": "^16.11.41", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-helmet-async": "^1.0.3", "axios": "^0.27.2", "framer-motion": "^12.19.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-helmet-async": "^2.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "typescript": "^4.7.4", diff --git a/store-ui/src/App.tsx b/store-ui/src/App.tsx index 2d44952..0b072c3 100644 --- a/store-ui/src/App.tsx +++ b/store-ui/src/App.tsx @@ -1,38 +1,278 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom" -import Home from './pages/Home/Home' -import Product from './pages/Product/Product' -import Cart from './pages/Cart/Cart' -import Checkout from './pages/Checkout/Checkout' -import OrderConfirmation from './pages/OrderConfirmation/OrderConfirmation' -import UserPage from './pages/User/UserPage' -import UserListPage from './pages/User/UserListPage' -import { LoginPage, RegisterPage, ProfilePage } from './pages/Auth' -import { AdminDashboard } from './pages/Admin' -import Layout from './components/layout/Layout' -import { AuthProvider } from './contexts/AuthContext' -import SearchComponent from './components/SearchComponent' - -const App = (props: any) => { - return ( - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - ) +import React, { Suspense } from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { HelmetProvider } from 'react-helmet-async'; +import Layout from './components/layout/Layout'; +import { AuthProvider } from './contexts/AuthContext'; +import CircularProgress from '@mui/material/CircularProgress'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Alert from '@mui/material/Alert'; + +// Lazy load components for code splitting +const Home = React.lazy(() => import('./pages/Home/Home')); +const SearchComponent = React.lazy(() => import('./components/SearchComponent')); +const Product = React.lazy(() => import('./pages/Product/Product')); +const Cart = React.lazy(() => import('./pages/Cart/Cart')); +const Checkout = React.lazy(() => import('./pages/Checkout/Checkout')); +const OrderConfirmation = React.lazy(() => import('./pages/OrderConfirmation/OrderConfirmation')); + +// Auth pages - using dynamic import with named exports +const LoginPage = React.lazy(() => + import('./pages/Auth/LoginPage').then(module => ({ default: module.LoginPage })) +); +const RegisterPage = React.lazy(() => + import('./pages/Auth/RegisterPage').then(module => ({ default: module.RegisterPage })) +); +const ProfilePage = React.lazy(() => import('./pages/Auth/ProfilePage')); + +// Admin pages with error boundary +const AdminDashboard = React.lazy(() => import('./pages/Admin/AdminDashboard')); + +// User management pages +const UserPage = React.lazy(() => import('./pages/User/UserPage')); +const UserListPage = React.lazy(() => import('./pages/User/UserListPage')); + +// Enhanced Loading component with better UX +const LoadingFallback: React.FC<{ message?: string }> = ({ message = 'Loading...' }) => ( + + + + {message} + + +); + +// Error Boundary Component +class ErrorBoundary extends React.Component< + { children: React.ReactNode; fallback?: React.ReactNode }, + { hasError: boolean; error?: Error } +> { + constructor(props: { children: React.ReactNode; fallback?: React.ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + + // In production, send error to monitoring service + if (process.env.NODE_ENV === 'production') { + // Example: Sentry.captureException(error); + } + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( + + + + Something went wrong + + + We're sorry, but something unexpected happened. Please try refreshing the page. + + + + + + + ); + } + + return this.props.children; + } } -export default App \ No newline at end of file + +// Enhanced Suspense wrapper with error boundary +const SuspenseWrapper: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({ + children, + fallback +}) => ( + + }> + {children} + + +); + +function App() { + return ( + + + + + + {/* Home page - highest priority */} + }> + + + } + /> + + {/* Search page */} + }> + + + } + /> + + {/* Product pages */} + }> + + + } + /> + + {/* Cart and checkout flow */} + }> + + + } + /> + + }> + + + } + /> + + }> + + + } + /> + + {/* Auth pages - separate chunk */} + }> + + + } + /> + + }> + + + } + /> + + }> + + + } + /> + + {/* Admin pages - separate chunk for role-based access */} + }> + + + } + /> + + {/* User management pages */} + }> + + + } + /> + + }> + + + } + /> + + {/* Category routes - can be dynamically loaded */} + }> + + + } + /> + + {/* 404 fallback */} + + + 404 - Page Not Found + + + The page you're looking for doesn't exist. + + + } + /> + + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/store-ui/src/api/cart.tsx b/store-ui/src/api/cart.tsx index 5c30ba0..6152a5b 100644 --- a/store-ui/src/api/cart.tsx +++ b/store-ui/src/api/cart.tsx @@ -1,5 +1,8 @@ -import axiosClient, { cartUrl } from "./config" +import axios from 'axios'; import { getCachedUser } from "./users"; +import { cartUrl } from './config'; + +const axiosClient = axios.create(); // Helper function to get the current user's ID (email) for cart operations const getCurrentCustomerId = () => { @@ -12,17 +15,17 @@ export const getCart = async () => { const customerId = getCurrentCustomerId(); const response = await axiosClient.get(`${cartUrl}cart/${customerId}`); return response.data; - } catch (err: any) { - console.log(err); - throw err; + } catch (error) { + console.error("Error fetching cart:", error); + return null; } -}; +} export const addToCart = async (item: any) => { try { + const customerId = getCurrentCustomerId(); // Get current cart let cart = await getCart(); - const customerId = getCurrentCustomerId(); if (!cart || !cart.items) { cart = { customerId, items: [] }; @@ -37,14 +40,15 @@ export const addToCart = async (item: any) => { // Send updated cart const response = await axiosClient.post(`${cartUrl}cart`, cart); return response.data; - } catch (err: any) { - console.log(err); - throw err; + } catch (error) { + console.error("Error adding to cart:", error); + throw error; } -}; +} export const updateQuantity = async (productId: string, quantity: number) => { try { + const customerId = getCurrentCustomerId(); let cart = await getCart(); if (!cart || !cart.items) throw new Error('Cart not found'); cart.items = cart.items.map((item: any) => @@ -52,21 +56,38 @@ export const updateQuantity = async (productId: string, quantity: number) => { ); const response = await axiosClient.post(`${cartUrl}cart`, cart); return response.data; - } catch (err: any) { - console.log(err); - throw err; + } catch (error) { + console.error("Error updating cart item quantity:", error); + throw error; } -}; +} export const removeFromCart = async (productId: string) => { try { + const customerId = getCurrentCustomerId(); let cart = await getCart(); if (!cart || !cart.items) throw new Error('Cart not found'); cart.items = cart.items.filter((item: any) => item.productId !== productId); const response = await axiosClient.post(`${cartUrl}cart`, cart); return response.data; - } catch (err: any) { - console.log(err); - throw err; + } catch (error) { + console.error("Error removing item from cart:", error); + throw error; + } +} + +// New function to clear the entire cart +export const clearCart = async () => { + try { + const customerId = getCurrentCustomerId(); + // Empty the items array to clear the cart + const response = await axiosClient.post(`${cartUrl}cart`, { + customerId, + items: [] + }); + return response.data; + } catch (error) { + console.error("Error clearing cart:", error); + throw error; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/store-ui/src/api/users.tsx b/store-ui/src/api/users.tsx index c2109cf..c4240ce 100644 --- a/store-ui/src/api/users.tsx +++ b/store-ui/src/api/users.tsx @@ -1,6 +1,12 @@ // filepath: /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/store-ui/src/api/users.tsx +import axios from 'axios'; import axiosClient, { usersUrl } from "./config"; +// Constants for localStorage +export const USER_TOKEN_KEY = 'user_token'; +export const USER_INFO_KEY = 'user_info'; +export const USER_ADDRESSES_KEY = 'user_addresses'; + // Types for User data export interface User { id?: number; @@ -28,9 +34,30 @@ export interface RegisterData { password: string; } -// Store auth token in localStorage -const TOKEN_KEY = 'auth_token'; -const USER_KEY = 'current_user'; +export interface Address { + id: string; + firstName: string; + lastName: string; + address: string; + city: string; + state: string; + postalCode: string; + country: string; + email: string; + phone?: string; + isDefault?: boolean; +} + +// Add auth token to requests if available +axiosClient.interceptors.request.use(config => { + const token = localStorage.getItem(USER_TOKEN_KEY); + if (token) { + // Ensure headers object exists before setting properties + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); /** * Login user and get JWT token @@ -49,7 +76,7 @@ export const loginUser = async (credentials: LoginCredentials): Promise => { */ export const getCurrentUser = async (): Promise => { try { - const token = localStorage.getItem(TOKEN_KEY); + const token = localStorage.getItem(USER_TOKEN_KEY); if (!token) { throw new Error('No authentication token found'); } @@ -98,7 +125,7 @@ export const getCurrentUser = async (): Promise => { console.log('Processed user data with role:', userData); // Log processed data with role // Cache user in localStorage - localStorage.setItem(USER_KEY, JSON.stringify(userData)); + localStorage.setItem(USER_INFO_KEY, JSON.stringify(userData)); return userData; } catch (err: any) { @@ -111,22 +138,22 @@ export const getCurrentUser = async (): Promise => { * Logout user by removing token */ export const logoutUser = (): void => { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USER_KEY); + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_INFO_KEY); }; /** * Check if user is logged in */ export const isAuthenticated = (): boolean => { - return localStorage.getItem(TOKEN_KEY) !== null; + return localStorage.getItem(USER_TOKEN_KEY) !== null; }; /** * Get cached user data (without API call) */ export const getCachedUser = (): User | null => { - const userData = localStorage.getItem(USER_KEY); + const userData = localStorage.getItem(USER_INFO_KEY); if (!userData) { console.log('No cached user data found in localStorage'); return null; @@ -150,7 +177,7 @@ export const getCachedUser = (): User | null => { } catch (error) { console.error('Error parsing cached user data:', error); // If there's an error parsing, clear the cache and return null - localStorage.removeItem(USER_KEY); + localStorage.removeItem(USER_INFO_KEY); return null; } }; @@ -220,4 +247,104 @@ export const updateUser = async (userId: number, userData: Partial): Promi console.error(`Error updating user ${userId}:`, err); throw err; } +}; + +// Get user's saved addresses +export const getUserAddresses = async (): Promise => { + // First check if addresses are in localStorage + const cachedAddresses = localStorage.getItem(USER_ADDRESSES_KEY); + if (cachedAddresses) { + return JSON.parse(cachedAddresses); + } + + try { + // If not in localStorage, fetch from API + const response = await axiosClient.get(`${usersUrl}/addresses`); + const addresses = response.data; + + // Cache addresses in localStorage + localStorage.setItem(USER_ADDRESSES_KEY, JSON.stringify(addresses)); + + return addresses; + } catch (error) { + console.error('Error fetching user addresses:', error); + // Return empty array if request fails + return []; + } +}; + +// Save a new address for the user +export const saveUserAddress = async (addressData: Partial
): Promise
=> { + try { + // In a real app, this would send a POST request to your API + // For now, we'll simulate an API response + + // Get existing addresses + const existingAddresses = await getUserAddresses(); + + // Create new address with ID + const newAddress: Address = { + id: `addr_${Date.now()}`, + firstName: addressData.firstName || '', + lastName: addressData.lastName || '', + address: addressData.address || '', + city: addressData.city || '', + state: addressData.state || '', + postalCode: addressData.postalCode || '', + country: addressData.country || 'USA', + email: addressData.email || '', + phone: addressData.phone || '', + isDefault: existingAddresses.length === 0 // First address is default + }; + + // Save to "API" (localStorage in this case) + const updatedAddresses = [...existingAddresses, newAddress]; + localStorage.setItem(USER_ADDRESSES_KEY, JSON.stringify(updatedAddresses)); + + return newAddress; + } catch (error) { + console.error('Error saving user address:', error); + throw new Error('Failed to save address'); + } +}; + +// Delete a saved address +export const deleteUserAddress = async (addressId: string): Promise => { + try { + // Get existing addresses + const existingAddresses = await getUserAddresses(); + + // Filter out the address to delete + const updatedAddresses = existingAddresses.filter(addr => addr.id !== addressId); + + // Save updated list back to localStorage + localStorage.setItem(USER_ADDRESSES_KEY, JSON.stringify(updatedAddresses)); + + return true; + } catch (error) { + console.error('Error deleting user address:', error); + return false; + } +}; + +// Set an address as the default +export const setDefaultAddress = async (addressId: string): Promise => { + try { + // Get existing addresses + const existingAddresses = await getUserAddresses(); + + // Update isDefault flag for all addresses + const updatedAddresses = existingAddresses.map(addr => ({ + ...addr, + isDefault: addr.id === addressId + })); + + // Save updated list back to localStorage + localStorage.setItem(USER_ADDRESSES_KEY, JSON.stringify(updatedAddresses)); + + return true; + } catch (error) { + console.error('Error setting default address:', error); + return false; + } }; \ No newline at end of file diff --git a/store-ui/src/components/AppBar/AppBar.tsx b/store-ui/src/components/AppBar/AppBar.tsx index 45880e8..63d0be0 100644 --- a/store-ui/src/components/AppBar/AppBar.tsx +++ b/store-ui/src/components/AppBar/AppBar.tsx @@ -24,7 +24,8 @@ import { alpha, Collapse, Fade, - Zoom + Zoom, + Chip } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; import { @@ -44,7 +45,8 @@ import { Storefront as StorefrontIcon, Phone as PhoneIcon, Help as HelpIcon, - LocalShipping as LocalShippingIcon + LocalShipping as LocalShippingIcon, + Close as CloseIcon } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import { useCart } from '../layout/CartContext'; @@ -54,27 +56,24 @@ import ThemeContext from '../layout/ThemeContext'; // Styled components const StyledAppBar = styled(MuiAppBar)(({ theme }) => ({ backdropFilter: 'blur(10px)', - backgroundColor: alpha(theme.palette.background.paper, 0.9), - boxShadow: '0 4px 20px rgba(0,0,0,0.07)', + backgroundColor: '#263238', // Dark navy/gray color from screenshot + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`, - color: theme.palette.text.primary, + color: '#FFFFFF', transition: 'all 0.3s ease' })); const Logo = styled(Typography)(({ theme }) => ({ - fontWeight: 700, + fontWeight: 600, letterSpacing: '0.5px', - background: theme.palette.mode === 'dark' - ? 'linear-gradient(45deg, #f06, #3cf)' - : 'linear-gradient(45deg, #3f51b5, #f50057)', - WebkitBackgroundClip: 'text', - WebkitTextFillColor: 'transparent', - fontSize: '1.5rem', + color: '#FF9800', // Orange color from the screenshot + fontSize: '1.4rem', display: 'flex', alignItems: 'center', - textTransform: 'uppercase' + cursor: 'pointer' })); +// Add keyboard focus styles to NavLink const NavLink = styled(Button)(({ theme }) => ({ textTransform: 'none', fontWeight: 500, @@ -91,8 +90,12 @@ const NavLink = styled(Button)(({ theme }) => ({ transition: 'all 0.3s ease', transform: 'translateX(-50%)', }, - '&:hover::after': { + '&:hover::after, &:focus-visible::after': { width: '70%', + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '2px', } })); @@ -136,10 +139,15 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({ }, })); +// Add keyboard focus styles to ProfileButton const ProfileButton = styled(IconButton)(({ theme }) => ({ transition: 'transform 0.2s ease', '&:hover': { transform: 'scale(1.05)', + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '2px', } })); @@ -189,6 +197,15 @@ const MenuButton = styled(Button)(({ theme }) => ({ } })); +// Enhance the ListItemButton keyboard accessibility +const StyledListItemButton = styled(ListItemButton)(({ theme }) => ({ + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '-2px', + backgroundColor: alpha(theme.palette.primary.main, 0.1), + } +})); + const AppBar = () => { const navigate = useNavigate(); const { cartCount } = useCart(); @@ -214,6 +231,13 @@ const AppBar = () => { const [openCategories, setOpenCategories] = useState(false); const [openHelp, setOpenHelp] = useState(false); + // Add this at the beginning of the component + const [searchQuery, setSearchQuery] = useState(''); + const searchInputRef = React.useRef(null); + + // Add state for screen reader announcements + const [announcement, setAnnouncement] = useState(''); + const handleProfileClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -264,29 +288,105 @@ const AppBar = () => { { name: 'FAQ', path: '/help/faq' } ]; + // Add keyboard shortcut for search focus + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Focus search input when user presses '/' key + if (event.key === '/' && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + searchInputRef.current?.focus(); + setAnnouncement('Search box is now focused. Type to search products.'); + } + + // Add shortcut to go to cart (c key) + if (event.key === 'c' && (event.ctrlKey || event.metaKey) && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + navigate('/cart'); + setAnnouncement('Navigated to shopping cart'); + } + + // Add shortcut to go to home (h key) + if (event.key === 'h' && (event.ctrlKey || event.metaKey) && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + navigate('/'); + setAnnouncement('Navigated to home page'); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [navigate]); + + // Clear announcement after it's been read + useEffect(() => { + if (announcement) { + const timer = setTimeout(() => { + setAnnouncement(''); + }, 3000); + + return () => clearTimeout(timer); + } + }, [announcement]); + + // Handle search submission + const handleSearchSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (searchQuery.trim()) { + navigate(`/search?q=${encodeURIComponent(searchQuery)}`); + setSearchQuery(''); + setDrawerOpen(false); + } + }; + const renderMobileDrawer = () => ( - + handleNavigation('/')}> EShop + + + {isLoggedIn && ( - + {getInitial()} @@ -302,133 +402,153 @@ const AppBar = () => { )} - + - handleNavigation('/')}> + handleNavigation('/')} + aria-label="Home page" + sx={{ py: 1 }} + > - + - setOpenCategories(!openCategories)}> + setOpenCategories(!openCategories)} + aria-expanded={openCategories} + aria-controls="categories-submenu" + sx={{ py: 1 }} + > - {openCategories ? : } - + {openCategories ? - - + + {categories.map((category) => ( - handleNavigation(category.path)} + aria-label={`Browse ${category.name} category`} > - - - ))} - - - - - setOpenHelp(!openHelp)}> - - - - - {openHelp ? : } - - - - - - {helpLinks.map((link) => ( - - handleNavigation(link.path)} - > - - + ))} - - handleNavigation('/contact')}> - - - - - - - {isLoggedIn ? ( <> - handleNavigation('/profile')}> + handleNavigation('/profile')} + sx={{ py: 1 }} + > - + - handleNavigation('/orders')}> + handleNavigation('/orders')} + sx={{ py: 1 }} + > - + + + + + handleNavigation('/cart')} + sx={{ py: 1 }} + > + + + + + My Cart + {cartCount > 0 && ( + + )} + + } + /> + {user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin' ? ( - handleNavigation('/admin')}> + handleNavigation('/admin')} + sx={{ py: 1 }} + > - + ) : null} - + - + ) : ( <> - handleNavigation('/login')}> + handleNavigation('/login')} + sx={{ py: 1 }} + > - + - handleNavigation('/register')}> + handleNavigation('/register')} + sx={{ py: 1 }} + > - + )} @@ -450,371 +570,339 @@ const AppBar = () => { ); return ( - - - - - {isMobile && ( + + {/* Add live region for screen reader announcements */} +
+ {announcement} +
+ + + + {isMobile ? ( + <> - )} - - - + navigate('/')} + tabIndex={0} + role="button" + aria-label="Go to homepage" + sx={{ fontSize: '1.25rem' }} + > + EShop + +
+ + + navigate('/cart')} + aria-label={`Shopping Cart with ${cartCount} items`} + size="small" + sx={{ ml: 0.5 }} + > + + + + + + + ) : ( + <> + navigate('/')} + sx={{ mr: 2, cursor: 'pointer' }} + tabIndex={0} + role="button" + aria-label="Go to homepage" + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + navigate('/'); + } }} - onClick={() => navigate('/')} > EShop - - - {!isMobile && ( - <> - - navigate('/')}>Home - - - } - onClick={() => setOpenCategories(!openCategories)} - > - Categories - - {openCategories && ( - - - {categories.map((category) => ( - { - navigate(category.path); - setOpenCategories(false); - }} - sx={{ py: 1.5 }} - > - {category.name} - - ))} - - - )} - - - navigate('/deals')}>Deals - navigate('/new-arrivals')}>New Arrivals - - - } - onClick={() => setOpenHelp(!openHelp)} - > - Help - - {openHelp && ( - - - {helpLinks.map((link) => ( - { - navigate(link.path); - setOpenHelp(false); - }} - sx={{ py: 1.5 }} - > - {link.name} - - ))} - - - )} - - + + + navigate('/')} + aria-label="Home page" + > + Home + + {/* Add keyboard accessibility to the menu items */} + {categories.map((category) => ( + navigate(category.path)} + aria-label={`Browse ${category.name} category`} + > + {category.name} + + ))} + + +
- + setSearchQuery(e.target.value)} /> + {/* Add a tooltip to show the keyboard shortcut */} + - - )} - - - - - {!isMobile && ( - + + + + {theme.palette.mode === 'dark' ? : } - )} - - {/* Admin Users Icon - Only visible to admin users */} - {isLoggedIn && (user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin') && ( - + + + navigate('/favorites')} + > + + + + + navigate('/users')} - sx={{ mr: 1 }} + onClick={() => navigate('/cart')} + aria-label={`Shopping Cart with ${cartCount} items`} > - + + + - )} - - - navigate('/wishlist')}> - - - - - - navigate('/cart')} - sx={{ mx: 0.5 }} - > - - - - - - - {isLoggedIn ? ( - - - + + + + {getInitial()} + + + + - navigate('/profile')} + aria-label="View your profile" + > + + + + Profile + + + navigate('/orders')} + aria-label="View your orders" > - {getInitial()} - - - - - - - {user?.name} - - - {user?.email} - - - - navigate('/profile')} sx={{ py: 1.5 }}> - - - - - - navigate('/orders')} sx={{ py: 1.5 }}> - - - - - - {user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin' ? ( - navigate('/admin')} sx={{ py: 1.5 }}> - + - + Orders - ) : null} - navigate('/settings')} sx={{ py: 1.5 }}> - - - - - - - - - - - - - - - ) : ( - - {!isMobile && ( - <> - - - - )} - {isMobile && ( - navigate('/login')} - > - - - )} - - )} - - - - + + + + Logout + + + + ) : ( + + )} + + + )} + + - {isMobile && renderMobileDrawer()} + {/* Mobile drawer */} + {renderMobileDrawer()} - {/* Optional secondary navbar for categories on desktop */} - {!isMobile && ( - - - - {categories.map((category) => ( - - ))} + {/* Add keyboard shortcuts help dialog */} + + + Keyboard Shortcuts: +
  • / - Focus search
  • +
  • Ctrl+H - Home page
  • +
  • Ctrl+C - Cart page
  • -
    -
    - )} -
    + } + arrow + placement="top-end" + > + + ? + +
    +
    + ); }; +// Add CSS for screen reader only content +const style = document.createElement('style'); +style.textContent = ` + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } +`; +document.head.appendChild(style); + export default AppBar; diff --git a/store-ui/src/components/Deals/Deals.tsx b/store-ui/src/components/Deals/Deals.tsx index 1231852..f08823d 100644 --- a/store-ui/src/components/Deals/Deals.tsx +++ b/store-ui/src/components/Deals/Deals.tsx @@ -9,74 +9,422 @@ import Chip from '@mui/material/Chip'; import Paper from '@mui/material/Paper'; import Link from '@mui/material/Link'; import StarIcon from '@mui/icons-material/Star'; +import CircularProgress from '@mui/material/CircularProgress'; +import Alert from '@mui/material/Alert'; import axiosClient, { productsUrl } from '../../api/config'; import { useState, useEffect } from 'react'; import { useNavigate } from "react-router-dom"; +import ImageOptimizer from '../ImageOptimizer'; +import { Button, IconButton, useTheme, useMediaQuery } from '@mui/material'; +import { addToCart } from '../../api/cart'; +import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; +import FavoriteIcon from '@mui/icons-material/Favorite'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; + +// Custom VisuallyHidden component for screen readers +const VisuallyHidden: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + +); + +// Define proper types for deals +interface Deal { + dealId: string; + variantSku: string; + name: string; + shortDescription: string; + thumbnail: string; + price: number; + rating: number; +} + +// Fallback image URLs for when thumbnails fail to load +const FALLBACK_IMAGES = { + product: '/assets/images/deals/mobile.webp', // Use one of your existing images as fallback + placeholder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDIwMCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNTAiIGZpbGw9IiNGNUY1RjUiLz48cGF0aCBkPSJNNzUgNzVMMTI1IDc1TTEwMCA1MEwxMDAgMTAwIiBzdHJva2U9IiNDQ0MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+' +}; + +// Helper function to get image source with fallback +const getImageSource = (thumbnail: string, dealName: string) => { + if (!thumbnail || thumbnail.trim() === '') { + return FALLBACK_IMAGES.product; + } + return thumbnail; +}; + +// Mock data for fallback when API returns empty results +const MOCK_DEALS: Deal[] = [ + { + dealId: 'mock-1', + variantSku: 'mock-sku-1', + name: 'Special Offer', + shortDescription: 'Great deals coming soon!', + thumbnail: '/assets/images/deals/mobile.webp', + price: 99.99, + rating: 4.5 + }, + { + dealId: 'mock-2', + variantSku: 'mock-sku-2', + name: 'Featured Product', + shortDescription: 'Amazing products at great prices', + thumbnail: '/assets/images/deals/oven.webp', + price: 199.99, + rating: 4.8 + }, + { + dealId: 'mock-3', + variantSku: 'mock-sku-3', + name: 'Best Seller', + shortDescription: 'Customer favorite items', + thumbnail: '/assets/images/deals/kurtha.webp', + price: 49.99, + rating: 4.3 + } +]; const Deals = () => { const navigate = useNavigate(); + const theme = useTheme(); + // Check if we're on a mobile device + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [deals, setDeals] = useState([]) - const [error, setError] = useState(null) + const [deals, setDeals] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [favorites, setFavorites] = React.useState>({}); const loadDeals = async () => { try { - const response = await axiosClient.get(productsUrl + 'deals') - setDeals(response.data) - setError(null) + setLoading(true); + setError(null); + const response = await axiosClient.get(productsUrl + 'deals'); + + // Handle empty response or invalid data structure + if (response.data && Array.isArray(response.data)) { + setDeals(response.data); + } else { + // If response is not an array or is null/undefined, set empty array + setDeals([]); + console.warn("Invalid deals data structure received:", response.data); + } } catch (err: any) { - setError(err) + setError(err); + setDeals([]); // Ensure deals is empty on error + console.error("Error fetching deals:", err); + } finally { + setLoading(false); } } // run on load useEffect(() => { - loadDeals() - }, []) + loadDeals(); + }, []); - return ( - - Deals of the Day - - <> - { - deals.slice(0, 5).map((deal: any) => ( - - { - navigate('product/' + deal.variantSku)} - } underline="none"> - - {deal.name} - - - - - {deal.shortDescription} - - - - - - - - $ {deal.price} - - } label={deal.rating} /> - - - - - - - + // Handle keyboard navigation for card links + const handleCardKeyPress = (event: React.KeyboardEvent, variantSku: string) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + navigate(`/product/${variantSku}`); + } + }; - )) + // Retry loading if there was an error + const handleRetry = () => { + loadDeals(); + }; + + const handleAddToCart = (product: any) => { + const item = { + productId: product?._id || product?.id || `product-${Math.random().toString(36).substr(2, 9)}`, + sku: product?.variants?.[0]?.sku || `sku-${Math.random().toString(36).substr(2, 9)}`, + title: product?.title, + quantity: 1, + price: product?.price, + currency: product?.currency || 'USD' + }; + addToCart(item); + }; + + const toggleFavorite = (id: string) => { + setFavorites(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + if (loading) { + return ( + + + Loading deals of the day + + ); + } + + if (error) { + return ( + + + Retry + } - - - - ) -} + > + Failed to load deals. Please try again later. + + + ); + } + + // Use mock data if no deals are available + const displayDeals = deals.length > 0 ? deals : MOCK_DEALS; + + return ( + + + + Deals of the Day + + + Limited Time Offers + + + + + + {displayDeals.length === 0 ? ( + + No deals available at the moment. + + ) : ( + displayDeals.slice(0, 5).map((deal: Deal, index: number) => ( + + + {index === 1 && ( + + )} + + toggleFavorite(deal.dealId)} + sx={{ + position: 'absolute', + top: 5, + right: index === 1 ? (isMobile ? 40 : 60) : 5, + backgroundColor: 'rgba(255,255,255,0.8)', + width: isMobile ? 24 : 32, + height: isMobile ? 24 : 32, + padding: isMobile ? '4px' : '8px', + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.9)', + } + }} + aria-label={favorites[deal.dealId] ? "Remove from favorites" : "Add to favorites"} + > + {favorites[deal.dealId] ? ( + + ) : ( + + )} + + + navigate(`/product/${deal.variantSku}`)} + sx={{ + cursor: 'pointer', + pt: 1, + px: 1, + pb: 0 + }} + > + + + + + + + navigate(`/product/${deal.variantSku}`)} + > + {deal.name} + + + + ${typeof deal.price === 'number' + ? deal.price.toFixed(2) + : parseFloat(String(deal.price || 0)).toFixed(2)} + + + + + + + )) + )} + + + + ); +}; -export default Deals \ No newline at end of file +export default Deals; \ No newline at end of file diff --git a/store-ui/src/components/ImageOptimizer.tsx b/store-ui/src/components/ImageOptimizer.tsx new file mode 100644 index 0000000..ea955c1 --- /dev/null +++ b/store-ui/src/components/ImageOptimizer.tsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Box from '@mui/material/Box'; +import Skeleton from '@mui/material/Skeleton'; +import { styled } from '@mui/material/styles'; +import { Typography } from '@mui/material'; + +interface ImageOptimizerProps { + src: string; + alt: string; + width?: string | number; + height?: string | number; + sizes?: string; + priority?: boolean; + className?: string; + style?: React.CSSProperties; + objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; + quality?: number; + placeholder?: 'blur' | 'empty'; + blurDataURL?: string; + // New props for better accessibility + longDesc?: string; + isDecorative?: boolean; + role?: string; +} + +// Styled component for the image container to maintain aspect ratio +const AspectRatioBox = styled(Box)<{ ratio?: string }>(({ ratio = '1/1' }) => ({ + position: 'relative', + width: '100%', + '&::before': { + content: '""', + display: 'block', + paddingTop: `calc(100% / (${ratio.split('/')[0]} / ${ratio.split('/')[1]}))`, + } +})); + +// Styled component for the image with object-fit property +const StyledImage = styled('img')<{ $objectFit: string }>(({ $objectFit }) => ({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + objectFit: $objectFit as any, +})); + +/** + * Simplified ImageOptimizer component that ensures images load properly + * Focuses on reliability over complex optimizations + */ +const ImageOptimizer: React.FC = ({ + src, + alt, + width = '100%', + height = 'auto', + sizes = '100vw', + priority = false, + className = '', + style = {}, + objectFit = 'cover', + quality = 80, + placeholder = 'empty', + blurDataURL, + longDesc = '', + isDecorative = false, + role = 'img' +}) => { + const [isLoaded, setIsLoaded] = useState(false); + const [hasError, setHasError] = useState(false); + const imgRef = useRef(null); + + // Handle image loading error + const handleError = () => { + console.error('Image failed to load:', src); + setHasError(true); + setIsLoaded(true); // Mark as loaded to remove skeleton + }; + + // Handle successful image load + const handleLoad = () => { + setIsLoaded(true); + }; + + // Determine appropriate alt text handling based on image purpose + const getImgProps = () => { + if (isDecorative) { + return { + alt: "", + "aria-hidden": true + }; + } else if (alt) { + return { + alt: alt, + "aria-hidden": false + }; + } else { + return { + alt: "Image", + "aria-hidden": false + }; + } + }; + + return ( + + {/* Loading skeleton - only show while image is loading */} + {!isLoaded && !hasError && ( + + )} + + {/* Error state */} + {hasError ? ( + + + Image not available + + + ) : ( + /* Actual image - always render but may be hidden by skeleton */ + + )} + + {/* Hidden description for complex images (for screen readers) */} + {longDesc && !isDecorative && ( + + {longDesc} + + )} + + ); +}; + +export default ImageOptimizer; \ No newline at end of file diff --git a/store-ui/src/components/SEO.tsx b/store-ui/src/components/SEO.tsx new file mode 100644 index 0000000..c01d65d --- /dev/null +++ b/store-ui/src/components/SEO.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Helmet } from 'react-helmet-async'; + +interface SEOProps { + title?: string; + description?: string; + keywords?: string; + imageUrl?: string; + image?: string; // Add alias for imageUrl + url?: string; + type?: string; + schema?: object; + preconnectUrls?: string[]; + noindex?: boolean; // Add noindex prop + meta?: Array<{ name: string; content: string; }>; +} + +/** + * SEO component for dynamically setting meta tags for better search engine optimization + * Implements proper Open Graph and Twitter Card meta tags for social sharing + */ +const SEO: React.FC = ({ + title = 'E-Commerce Store - Quality Products at Great Prices', + description = 'Shop our wide selection of products. Find great deals on electronics, fashion, home goods, and more.', + keywords = 'e-commerce, online shopping, electronics, fashion, home goods', + imageUrl = '/logo.png', + image, // Destructure image prop + url = window.location.href, + type = 'website', + schema, + preconnectUrls = [ + 'https://fonts.googleapis.com', + 'https://fonts.gstatic.com', + 'https://cdn.jsdelivr.net' + ], + noindex, // Destructure noindex prop + meta, // Destructure meta prop +}) => { + // Use image prop if provided, otherwise use imageUrl + const finalImageUrl = image || imageUrl; + + // Convert relative image URL to absolute + const absoluteImageUrl = finalImageUrl.startsWith('http') + ? finalImageUrl + : `${window.location.origin}${finalImageUrl}`; + + // Format the JSON-LD schema data + const schemaData = schema + ? JSON.stringify(schema) + : JSON.stringify({ + "@context": "https://schema.org", + "@type": "WebSite", + "name": title, + "description": description, + "url": url, + }); + + return ( + + {/* Basic meta tags */} + {title} + + + + + {/* Preconnect for external resources to improve performance */} + {preconnectUrls.map((url, index) => ( + + ))} + + {/* Font display optimization */} + + + {/* Open Graph meta tags for social sharing (Facebook, LinkedIn) */} + + + + + + + + {/* Twitter Card meta tags */} + + + + + + {/* JSON-LD structured data */} + + + {/* Additional meta tags from meta prop */} + {meta && meta.map((m, index) => ( + + ))} + + {/* Noindex tag */} + {noindex && } + + ); +}; + +export default SEO; \ No newline at end of file diff --git a/store-ui/src/components/SearchComponent.tsx b/store-ui/src/components/SearchComponent.tsx index 0b1660c..2d541ab 100644 --- a/store-ui/src/components/SearchComponent.tsx +++ b/store-ui/src/components/SearchComponent.tsx @@ -1,302 +1,329 @@ -import React, { useState, useEffect } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { searchUrl } from '../api/config'; +import { + Box, + TextField, + Button, + Typography, + Paper, + InputAdornment, + Chip, + CircularProgress, + Grid, + Card, + CardMedia, + CardContent, + CardActionArea, + Divider, + useTheme, + List, + ListItem, + ListItemText, + InputBase, + IconButton, + styled, + alpha +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import TuneIcon from '@mui/icons-material/Tune'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import HistoryIcon from '@mui/icons-material/History'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import LocalFireDepartmentIcon from '@mui/icons-material/LocalFireDepartment'; +import CategoryIcon from '@mui/icons-material/Category'; +import BookmarkIcon from '@mui/icons-material/Bookmark'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import CloseIcon from '@mui/icons-material/Close'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; + +// Styled search input similar to the screenshot +const SearchInput = styled(InputBase)(({ theme }) => ({ + width: '100%', + fontSize: '0.95rem', + padding: '8px 16px', + transition: theme.transitions.create('width'), + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.1) : '#f5f5f5', + borderRadius: 4, + '&:hover': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.15) : '#e5e5e5', + }, + '&.Mui-focused': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.15) : '#e5e5e5', + } +})); + +const SearchResults = styled(Paper)(({ theme }) => ({ + position: 'absolute', + width: '100%', + maxHeight: '400px', + overflow: 'auto', + zIndex: 1300, + marginTop: '4px', + borderRadius: 4, + boxShadow: '0 4px 20px rgba(0,0,0,0.15)' +})); + +const ProductCard = styled(Card)(({ theme }) => ({ + display: 'flex', + borderRadius: 4, + boxShadow: 'none', + border: `1px solid ${alpha(theme.palette.divider, 0.5)}`, + '&:hover': { + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + } +})); interface SearchResult { id: string; title: string; - description: string; - category: string; + description?: string; price: number; - imageUrl?: string; - inStock: boolean; - rating: number; - score: number; - highlights?: any; -} - -interface SearchResponse { - total: number; - products: SearchResult[]; - query: { - searchTerm: string; - category?: string; - priceRange?: { min?: string; max?: string }; - pagination: { limit: number; offset: number }; - }; + thumbnail?: string; } const SearchComponent: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [searchTerm, setSearchTerm] = useState(searchParams.get('query') || ''); - const [results, setResults] = useState(null); - const [suggestions, setSuggestions] = useState([]); - const [loading, setLoading] = useState(false); - const [category, setCategory] = useState(searchParams.get('category') || ''); - const [minPrice, setMinPrice] = useState(searchParams.get('minPrice') || ''); - const [maxPrice, setMaxPrice] = useState(searchParams.get('maxPrice') || ''); + const [query, setQuery] = useState(searchParams.get('query') || ''); + const [results, setResults] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showDropdown, setShowDropdown] = useState(false); + const navigate = useNavigate(); + const inputRef = useRef(null); + const theme = useTheme(); + + // Debounce search suggestions + const debounceTimeout = useRef(null); + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault(); + if (query.trim()) { + navigate(`/search?query=${encodeURIComponent(query.trim())}`); + setShowDropdown(false); + } + }; - // Search function - const handleSearch = async (queryOverride?: string) => { - const query = queryOverride || searchTerm; - if (!query.trim()) return; + const fetchSuggestions = useCallback(async (searchTerm: string) => { + if (!searchTerm || searchTerm.length < 2) { + setSuggestions([]); + return; + } - setLoading(true); try { - const params = new URLSearchParams({ - query: query, - limit: '10', - ...(category && { category }), - ...(minPrice && { minPrice }), - ...(maxPrice && { maxPrice }) - }); - - // Update URL params - setSearchParams(params); - - console.log('Making search API call to:', `${searchUrl}api/search?${params}`); - - const response = await fetch(`${searchUrl}api/search?${params}`); - const data = await response.json(); + setLoading(true); + const response = await fetch(`${searchUrl}?query=${encodeURIComponent(searchTerm)}&limit=5`); - console.log('Search API response:', response.status, data); - - if (response.ok) { - setResults(data); - } else { - console.error('Search error:', data); + if (!response.ok) { + throw new Error('Search failed'); } - } catch (error) { - console.error('Search failed:', error); - } finally { + + const data = await response.json(); + // Extract unique terms from results + const terms = data.results.map((item: any) => item.title).slice(0, 5); + setSuggestions(terms); + setLoading(false); + setError(null); + } catch (err) { + console.error('Error fetching suggestions:', err); + setError('Failed to get suggestions'); setLoading(false); } - }; - - // Handle button click - const handleSearchClick = () => { - handleSearch(); - }; + }, []); - // Auto-search when component mounts with query param - useEffect(() => { - const queryFromUrl = searchParams.get('query'); - if (queryFromUrl) { - setSearchTerm(queryFromUrl); - handleSearch(queryFromUrl); + const debounceFetchSuggestions = (searchTerm: string) => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); } - }, []); + + debounceTimeout.current = setTimeout(() => { + fetchSuggestions(searchTerm); + }, 300); + }; - // Get suggestions - const getSuggestions = async (query: string) => { - if (query.length < 2) { + const handleInputChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setQuery(value); + + if (value.trim().length > 1) { + setShowDropdown(true); + debounceFetchSuggestions(value); + } else { + setShowDropdown(false); setSuggestions([]); - return; } + }; - try { - console.log('Making suggestions API call to:', `${searchUrl}api/suggest?query=${encodeURIComponent(query)}&limit=5`); - - const response = await fetch(`${searchUrl}api/suggest?query=${encodeURIComponent(query)}&limit=5`); - const data = await response.json(); - - console.log('Suggestions API response:', response.status, data); - - if (response.ok) { - setSuggestions(data.suggestions || []); - } - } catch (error) { - console.error('Suggestions failed:', error); + const handleSuggestionClick = (suggestion: string) => { + setQuery(suggestion); + navigate(`/search?query=${encodeURIComponent(suggestion)}`); + setShowDropdown(false); + }; + + const handleFocus = () => { + if (query.trim().length > 1) { + setShowDropdown(true); } }; - // Handle input change with debounced suggestions - useEffect(() => { - const timer = setTimeout(() => { - getSuggestions(searchTerm); - }, 300); + const handleBlur = () => { + // Delay hiding dropdown to allow clicking suggestions + setTimeout(() => { + setShowDropdown(false); + }, 200); + }; - return () => clearTimeout(timer); - }, [searchTerm]); + const handleKeyDown = (event: React.KeyboardEvent) => { + // Handle keyboard navigation in dropdown + if (!showDropdown) return; + + switch (event.key) { + case 'Escape': + setShowDropdown(false); + break; + case 'ArrowDown': + // Navigate down in suggestions + // Implementation for keyboard navigation would go here + break; + case 'ArrowUp': + // Navigate up in suggestions + break; + case 'Enter': + // Submit form will be handled by the form's onSubmit + break; + } + }; - // Debug: Show current searchUrl - useEffect(() => { - console.log('Search service URL configured as:', searchUrl); - }, []); + // Popular search terms + const popularSearches = [ + 'sneakers', 'running shoes', 'women\'s flats', 'shoe rack', 'shoe polish' + ]; return ( -
    -

    πŸ” Product Search

    - - {/* Search Form */} -
    -
    -
    - setSearchTerm(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSearch()} - placeholder="Search products..." - style={{ - width: '100%', - padding: '10px', - border: '1px solid #ddd', - borderRadius: '4px', - fontSize: '16px' - }} - /> - - {/* Suggestions Dropdown */} - {suggestions.length > 0 && ( -
    - {suggestions.map((suggestion, index) => ( -
    { - setSearchTerm(suggestion.title); - setSuggestions([]); - }} - style={{ - padding: '10px', - cursor: 'pointer', - borderBottom: '1px solid #eee' - }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'white'} + +
    + + + + + } + endAdornment={ + query && ( + + setQuery('')} + edge="end" + aria-label="Clear search" > -
    {suggestion.title}
    -
    - {suggestion.category} - ${suggestion.price} -
    -
    - ))} -
    - )} -
    - - -
    + Search + + + - {/* Filters */} -
    - - - setMinPrice(e.target.value)} - placeholder="Min Price" - style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> - - setMaxPrice(e.target.value)} - placeholder="Max Price" - style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> -
    -
    - - {/* Results */} - {results && ( -
    -

    Search Results ({results.total} found)

    - - {results.products.length === 0 ? ( -

    No products found for "{results.query.searchTerm}"

    - ) : ( -
    - {results.products.map((product) => ( -
    + {loading ? ( + + + + ) : error ? ( + + {error} + + ) : suggestions.length > 0 ? ( + + {suggestions.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + role="option" + aria-selected={false} + divider > -

    - {product.highlights?.title ? ( - - ) : ( - product.title - )} -

    - -

    - {product.highlights?.description ? ( - - ) : ( - product.description - )} -

    - -
    - - ${product.price} - - - {product.inStock ? 'In Stock' : 'Out of Stock'} - -
    - -
    - Category: {product.category} | Rating: ⭐ {product.rating} | Score: {product.score?.toFixed(2)} -
    -
    + + + + ))} -
    + + ) : query.length > 1 ? ( + + + No suggestions found for "{query}" + + + ) : ( + + + Popular Searches + + + {popularSearches.map((term, index) => ( + handleSuggestionClick(term)} + sx={{ + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.2) : alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.main, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.3) : alpha(theme.palette.primary.main, 0.2), + } + }} + /> + ))} + + )} -
    + )} -
    +
    ); }; diff --git a/store-ui/src/components/layout/CartContext.tsx b/store-ui/src/components/layout/CartContext.tsx index 3c087cb..6b73759 100644 --- a/store-ui/src/components/layout/CartContext.tsx +++ b/store-ui/src/components/layout/CartContext.tsx @@ -1,49 +1,215 @@ -import React from 'react'; -import { getCart } from '../../api/cart'; -import { useAuth } from '../../contexts/AuthContext'; +import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; +import { getCart, updateQuantity, removeFromCart } from '../../api/cart'; + +interface CartItem { + productId: string; + sku: string; + title: string; + quantity: number; + price: number; + currency: string; + thumbnail?: string; +} interface CartContextType { + cart: { items: CartItem[] } | null; cartCount: number; - updateCartCount: () => void; + cartTotal: number; + loading: boolean; + error: string | null; + addToCart: (item: CartItem) => Promise; + updateItemQuantity: (sku: string, quantity: number) => Promise; + removeItem: (sku: string) => Promise; + refreshCart: () => Promise; + clearError: () => void; } -const CartContext = React.createContext({ - cartCount: 0, - updateCartCount: () => {} -}); - -export const CartProvider = ({ children }: { children: React.ReactNode }) => { - const [cartCount, setCartCount] = React.useState(0); - const { isLoggedIn, user } = useAuth(); - - const updateCartCount = React.useCallback(() => { - // Only fetch cart if user is logged in - if (isLoggedIn && user) { - getCart().then((cart) => { - const count = cart?.items?.reduce((total: number, item: any) => total + item.quantity, 0) || 0; - setCartCount(count); - }).catch((error) => { - console.error('Failed to update cart count:', error); - setCartCount(0); +const CartContext = createContext(undefined); + +export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [cart, setCart] = useState<{ items: CartItem[] } | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Memoized calculations for performance + const cartCount = useMemo(() => { + return cart?.items?.reduce((total, item) => total + item.quantity, 0) || 0; + }, [cart?.items]); + + const cartTotal = useMemo(() => { + return cart?.items?.reduce((total, item) => total + (item.price * item.quantity), 0) || 0; + }, [cart?.items]); + + // Clear error function + const clearError = useCallback(() => { + setError(null); + }, []); + + // Refresh cart data + const refreshCart = useCallback(async () => { + try { + setLoading(true); + setError(null); + const cartData = await getCart(); + setCart(cartData || { items: [] }); + } catch (err: any) { + setError(err.message || 'Failed to load cart'); + console.error('Error loading cart:', err); + } finally { + setLoading(false); + } + }, []); + + // Add item to cart with optimistic updates + const addToCart = useCallback(async (item: CartItem) => { + try { + setError(null); + + // Optimistic update - add item immediately to UI + setCart(prevCart => { + if (!prevCart) return { items: [item] }; + + const existingItemIndex = prevCart.items.findIndex(cartItem => cartItem.sku === item.sku); + + if (existingItemIndex >= 0) { + // Update existing item quantity + const updatedItems = [...prevCart.items]; + updatedItems[existingItemIndex] = { + ...updatedItems[existingItemIndex], + quantity: updatedItems[existingItemIndex].quantity + item.quantity + }; + return { items: updatedItems }; + } else { + // Add new item + return { items: [...prevCart.items, item] }; + } }); - } else { - // Reset cart count if not logged in - setCartCount(0); + + // Make API call + await updateQuantity(item.sku, item.quantity); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to add item to cart'); + // Revert optimistic update on error + await refreshCart(); + throw err; } - }, [isLoggedIn, user]); + }, [refreshCart]); - // Update cart count when auth state changes - React.useEffect(() => { - updateCartCount(); - }, [updateCartCount, isLoggedIn, user]); + // Update item quantity with optimistic updates + const updateItemQuantity = useCallback(async (sku: string, quantity: number) => { + const previousCartState = cart; // Declare at the beginning + + try { + setError(null); + + // Optimistic update + setCart(prevCart => { + if (!prevCart) return null; + + const updatedItems = prevCart.items.map(item => + item.sku === sku ? { ...item, quantity } : item + ).filter(item => item.quantity > 0); // Remove items with 0 quantity + + return { items: updatedItems }; + }); + + // Make API call + await updateQuantity(sku, quantity); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to update item quantity'); + // Revert optimistic update on error + setCart(previousCartState); + throw err; + } + }, [cart, refreshCart]); + + // Remove item from cart with optimistic updates + const removeItem = useCallback(async (sku: string) => { + const previousCartState = cart; // Declare at the beginning + + try { + setError(null); + + // Optimistic update + setCart(prevCart => { + if (!prevCart) return null; + + const updatedItems = prevCart.items.filter(item => item.sku !== sku); + return { items: updatedItems }; + }); + + // Make API call + await removeFromCart(sku); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to remove item from cart'); + // Revert optimistic update on error + setCart(previousCartState); + throw err; + } + }, [cart, refreshCart]); + + // Load cart on mount + useEffect(() => { + refreshCart(); + }, [refreshCart]); + + // Persist cart state to localStorage for offline resilience + useEffect(() => { + if (cart && cart.items.length > 0) { + try { + localStorage.setItem('cart_backup', JSON.stringify(cart)); + } catch (err) { + console.error('Failed to backup cart to localStorage:', err); + } + } + }, [cart]); + + // Load cart from localStorage on mount if available + useEffect(() => { + try { + const backupCart = localStorage.getItem('cart_backup'); + if (backupCart && !cart) { + const parsedCart = JSON.parse(backupCart); + setCart(parsedCart); + } + } catch (err) { + console.error('Failed to load cart backup from localStorage:', err); + } + }, []); + + const value = useMemo(() => ({ + cart, + cartCount, + cartTotal, + loading, + error, + addToCart, + updateItemQuantity, + removeItem, + refreshCart, + clearError + }), [cart, cartCount, cartTotal, loading, error, addToCart, updateItemQuantity, removeItem, refreshCart, clearError]); return ( - + {children} ); }; -export const useCart = () => React.useContext(CartContext); - -export default CartContext; +export const useCart = (): CartContextType => { + const context = useContext(CartContext); + if (!context) { + throw new Error('useCart must be used within a CartProvider'); + } + return context; +}; diff --git a/store-ui/src/components/layout/Header/Header.tsx b/store-ui/src/components/layout/Header/Header.tsx index 2a1fdf7..23c823b 100644 --- a/store-ui/src/components/layout/Header/Header.tsx +++ b/store-ui/src/components/layout/Header/Header.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import MuiAppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import Toolbar from '@mui/material/Toolbar'; @@ -7,7 +7,7 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import Badge from '@mui/material/Badge'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useCart } from '../CartContext'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import LightModeIcon from '@mui/icons-material/LightMode'; @@ -60,8 +60,61 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({ }, })); +// Styled skip link that properly handles focus state +const SkipLink = styled('a')(({ theme }) => ({ + position: 'absolute', + left: '-9999px', + top: 'auto', + width: '1px', + height: '1px', + overflow: 'hidden', + '&:focus': { + position: 'fixed', + top: '0', + left: '0', + width: 'auto', + height: 'auto', + padding: '12px', + backgroundColor: theme.palette.primary.main, + color: '#fff', + zIndex: 9999, + textDecoration: 'none', + fontWeight: 'bold', + borderRadius: '0 0 4px 0', + } +})); + +// Styled enhanced cart badge wrapper with improved touch target +const EnhancedCartBadge = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + // Larger touch target area (44px is accessibility recommendation) + minWidth: '44px', + minHeight: '44px', + borderRadius: '50%', + transition: 'background-color 0.2s', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + // Pulse animation when cart has items + '@keyframes pulse': { + '0%': { + boxShadow: '0 0 0 0 rgba(255, 255, 255, 0.2)', + }, + '70%': { + boxShadow: '0 0 0 8px rgba(255, 255, 255, 0)', + }, + '100%': { + boxShadow: '0 0 0 0 rgba(255, 255, 255, 0)', + }, + }, +})); + const Header = () => { const navigate = useNavigate(); + const location = useLocation(); const { cartCount } = useCart(); const theme = useTheme(); const colorMode = React.useContext(ThemeContext); @@ -69,6 +122,9 @@ const Header = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [currentTab, setCurrentTab] = useState(0); const [searchQuery, setSearchQuery] = useState(''); + const searchInputRef = useRef(null); + const menuButtonRef = useRef(null); + const firstFocusableElementRef = useRef(null); const categories = [ { name: 'Top Offers', path: '/' }, @@ -81,6 +137,61 @@ const Header = () => { { name: 'Toys', path: '/category/toys' } ]; + // Set active tab based on current route + useEffect(() => { + const currentPath = location.pathname; + + // Find the matching category index + const activeTabIndex = categories.findIndex(category => + // Check for exact match or if we're on the home page + category.path === currentPath || + // Special case for home page + (category.path === '/' && currentPath === '/') + ); + + // If found, set it as current tab + if (activeTabIndex !== -1) { + setCurrentTab(activeTabIndex); + } else { + // If on a product page or other page, try to match the category from URL + const categoryMatch = categories.findIndex(category => + currentPath.includes(category.path) && category.path !== '/' + ); + + if (categoryMatch !== -1) { + setCurrentTab(categoryMatch); + } else { + // Default to home tab if no match + setCurrentTab(0); + } + } + }, [location.pathname]); + + // Store tab state in session storage + useEffect(() => { + try { + sessionStorage.setItem('lastActiveTab', currentTab.toString()); + } catch (error) { + console.error('Failed to save tab state:', error); + } + }, [currentTab]); + + // Load tab state from session storage on initial render + useEffect(() => { + try { + const savedTab = sessionStorage.getItem('lastActiveTab'); + if (savedTab !== null) { + const tabIndex = parseInt(savedTab, 10); + // Only set if it's a valid index and not already set by the route matching + if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex < categories.length) { + setCurrentTab(tabIndex); + } + } + } catch (error) { + console.error('Failed to load tab state:', error); + } + }, []); + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setCurrentTab(newValue); navigate(categories[newValue].path); @@ -90,6 +201,23 @@ const Header = () => { setMobileMenuOpen(!mobileMenuOpen); }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setMobileMenuOpen(false); + } + }; + + // Focus management for mobile drawer + useEffect(() => { + if (mobileMenuOpen && firstFocusableElementRef.current) { + // Focus first element when drawer opens + firstFocusableElementRef.current.focus(); + } else if (!mobileMenuOpen && menuButtonRef.current) { + // Return focus to menu button when drawer closes + menuButtonRef.current.focus(); + } + }, [mobileMenuOpen]); + const handleSearch = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && searchQuery.trim()) { navigate(`/search?query=${encodeURIComponent(searchQuery.trim())}`); @@ -102,11 +230,15 @@ const Header = () => { return ( + {/* Skip to content link for accessibility */} + + Skip to main content + @@ -114,10 +246,13 @@ const Header = () => { {isMobile && ( @@ -136,6 +271,10 @@ const Header = () => { marginRight: 2 }} onClick={() => navigate('/')} + tabIndex={0} + role="link" + aria-label="Home page" + onKeyDown={(e: React.KeyboardEvent) => e.key === 'Enter' && navigate('/')} > E-Commerce Store @@ -147,10 +286,11 @@ const Header = () => { )} @@ -162,28 +302,72 @@ const Header = () => { color="inherit" onClick={colorMode.toggleColorMode} sx={{ ml: 1 }} - aria-label="toggle dark mode" + aria-label={theme.palette.mode === 'dark' ? "Switch to light mode" : "Switch to dark mode"} > {theme.palette.mode === 'dark' ? : } - navigate('/cart')} - sx={{ ml: 1 }} - aria-label="shopping cart" - > - - - - + {/* Enhanced Cart Badge */} + + navigate('/cart')} + aria-label={`Shopping cart with ${cartCount} ${cartCount === 1 ? 'item' : 'items'}`} + sx={{ + position: 'relative', + animation: cartCount > 0 ? 'pulse 2s infinite' : 'none', + minWidth: '44px', + minHeight: '44px', + }} + > + + + + + @@ -191,7 +375,7 @@ const Header = () => { {!isMobile && ( - + {/* Darker grey-blue instead of bright blue */} { scrollButtons="auto" textColor="inherit" indicatorColor="secondary" - aria-label="category navigation" + aria-label="Category navigation tabs" sx={{ '& .MuiTab-root': { minWidth: 100, fontSize: '0.875rem', fontWeight: 500, + color: '#FFFFFF', + '&:hover': { + backgroundColor: 'rgba(255, 152, 0, 0.1)', + }, + '&.Mui-selected': { + color: '#FF9800', + } }, '& .MuiTabs-indicator': { - backgroundColor: '#ffffff', + backgroundColor: '#FF9800', // Orange indicator instead of white + height: '3px' + }, + '& .MuiTabScrollButton-root': { + color: '#FFFFFF', } }} > {categories.map((category, index) => ( - + ))} @@ -221,42 +420,104 @@ const Header = () => { )} - {/* Mobile Navigation Drawer */} + {/* Mobile Navigation Drawer with improved accessibility */} setMobileMenuOpen(false)} + role="dialog" + aria-modal="true" + aria-label="Main navigation menu" + onKeyDown={handleKeyDown} + id="mobile-navigation-drawer" + ModalProps={{ + // Trap focus within the drawer + disableEnforceFocus: false, + // Return focus to menu button when closed + onClose: () => { + setMobileMenuOpen(false); + if (menuButtonRef.current) { + menuButtonRef.current.focus(); + } + } + }} > - + - + + {/* Hidden help text for screen readers */} + + Press Enter to search for products + {categories.map((category, index) => ( - navigate(category.path)}> - + + ))} +
    + {/* Main content will be here */} +
    ); } diff --git a/store-ui/src/components/layout/Layout.tsx b/store-ui/src/components/layout/Layout.tsx index 8cd22e1..27eb935 100644 --- a/store-ui/src/components/layout/Layout.tsx +++ b/store-ui/src/components/layout/Layout.tsx @@ -1,60 +1,250 @@ -import React from "react" +import React, { useContext } from "react" import Header from "./Header/Header" import Footer from "./Footer/Footer" import Box from '@mui/material/Box'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import ThemeContext from "./ThemeContext"; +import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material/styles'; +import { useMediaQuery } from '@mui/material'; +import ThemeContext, { ThemeProvider } from "./ThemeContext"; import GlobalContext from "./GlobalContext"; import { CartProvider } from "./CartContext"; +import CssBaseline from '@mui/material/CssBaseline'; -const Layout = (props: any) => { - const [mode, setMode] = React.useState<'light' | 'dark'>('light'); +// Skip link component for keyboard accessibility +const SkipLink = () => { + return ( + { + e.currentTarget.style.top = '0'; + }} + onBlur={(e) => { + e.currentTarget.style.top = '-40px'; + }} + > + Skip to main content + + ); +}; +const Layout = (props: any) => { const [data, setData] = React.useState({}); const value = React.useMemo( () => ({ data, setData }), [data] ); - const colorMode = React.useMemo( - () => ({ - toggleColorMode: () => { - setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); - }, - }), - [], + return ( + + + + + ); +}; + +// Separate component to consume the theme context +const LayoutContent = (props: any) => { + const themeContext = useContext(ThemeContext); + + // Detect system preference for dark mode + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + // Use system preference if mode hasn't been set by user + React.useEffect(() => { + if (themeContext.mode === 'light' && prefersDarkMode) { + // Only auto-switch to dark if user hasn't explicitly set light mode + const userPreference = localStorage.getItem('themeMode'); + if (!userPreference) { + themeContext.toggleColorMode(); + } + } + }, [prefersDarkMode, themeContext]); const theme = React.useMemo( - () => - createTheme({ - palette: { - mode, + () => createTheme({ + palette: { + mode: themeContext.mode, + primary: { + main: '#FF9800', // Orange from screenshot + light: '#FFB74D', + dark: '#F57C00', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#263238', // Dark blue/gray from screenshot header + light: '#4F5B62', + dark: '#000A12', + contrastText: '#FFFFFF', + }, + background: { + default: themeContext.mode === 'dark' ? '#121212' : '#F5F5F5', + paper: themeContext.mode === 'dark' ? '#1E1E1E' : '#FFFFFF', + }, + text: { + primary: themeContext.mode === 'dark' ? '#FFFFFF' : '#212121', + secondary: themeContext.mode === 'dark' ? '#B0BEC5' : '#757575', + }, + divider: themeContext.mode === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)', + error: { + main: '#F44336', // Error red + }, + warning: { + main: '#FFC107', // Warning yellow + }, + info: { + main: '#2196F3', // Info blue + }, + success: { + main: '#4CAF50', // Success green + }, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + scrollbarColor: themeContext.mode === 'dark' ? "#6b6b6b #2b2b2b" : "#959595 #f5f5f5", + "&::-webkit-scrollbar, & *::-webkit-scrollbar": { + backgroundColor: themeContext.mode === 'dark' ? "#2b2b2b" : "#f5f5f5", + width: '8px', + }, + "&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb": { + borderRadius: 4, + backgroundColor: themeContext.mode === 'dark' ? "#6b6b6b" : "#959595", + minHeight: 24, + } + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: '#263238', // Dark color from screenshot header + color: '#ffffff', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: '4px', + textTransform: 'none', + fontWeight: 500, + }, + contained: { + boxShadow: 'none', + '&:hover': { + boxShadow: '0 2px 4px rgba(0,0,0,0.2)', + }, + }, + containedPrimary: { + backgroundColor: '#FF9800', // Orange button from screenshot + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#F57C00', + }, + }, + outlined: { + borderColor: themeContext.mode === 'dark' ? 'rgba(255,255,255,0.23)' : 'rgba(0,0,0,0.23)', + }, + }, }, - }), - [mode], + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + elevation1: { + boxShadow: '0 1px 3px rgba(0,0,0,0.12)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + borderRadius: '4px', + }, + }, + }, + }, + }, + typography: { + fontFamily: [ + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + h1: { + fontWeight: 500, + }, + h2: { + fontWeight: 500, + }, + h3: { + fontWeight: 500, + }, + h4: { + fontWeight: 500, + }, + h5: { + fontWeight: 500, + }, + h6: { + fontWeight: 500, + }, + button: { + fontWeight: 500, + }, + }, + shape: { + borderRadius: 4, + }, + }), + [themeContext.mode] ); return ( - - - - -
    - - {props.children} - -