From 869d4415dd0ee0d8ee1e6ed831d0246c918edab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bedr=CC=8Cich=20Schindler?= Date: Fri, 27 Mar 2026 15:40:33 +0100 Subject: [PATCH] Introduce `devcontainer` service container based development environment (#705) All development is now done inside the container named `devcontainer` which contains all necessary tools and dependencies. The devcontainer orchestrates other service containers behind the scenes via Docker-from-Docker. Other containers are implementation details and should not be accessed directly. This is breaking change commit for development environment, so it is required to remove all the env files and start with fresh one. There are two supported ways to access the development environment. Recommended way is to use Development Containers with an IDE. The alternative is to use Docker Compose directly. Local development is officially no more supported. What more, documentation of contribution and CLAUDE.md was updated to match with the changes. --- .devcontainer/devcontainer.json | 23 ++ .env.dist | 50 +++- .env.playwright.dist | 9 - .gitignore | 4 +- CLAUDE.md | 18 +- docker-compose.base.yml | 93 +++++++ docker-compose.yml | 43 ---- docker-compose.yml.dist | 41 ++++ docker/build-docker-images.sh | 26 ++ docker/mkdocs/Dockerfile | 4 +- docker/react_ui_devcontainer/Dockerfile | 129 ++++++++++ .../files/home/developer/shell-init.fish | 16 ++ .../files/home/developer/shell-init.sh | 13 + .../files/usr/local/bin/claude | 3 + .../files/usr/local/bin/copilot | 3 + .../files/usr/local/bin/docker-entrypoint | 10 + .../files/usr/local/bin/mkdocs | 5 + .../files/usr/local/bin/node | 5 + .../files/usr/local/bin/npm | 9 + .../files/usr/local/bin/npx | 5 + .../files/usr/local/bin/opencode | 3 + .../Dockerfile.dist | 4 + package.json | 2 +- scripts/auto-start-mkdocs.sh | 5 + scripts/auto-start-node.sh | 33 +++ scripts/write-lockfile-hash.sh | 15 ++ setup.sh | 104 ++++++++ src/docs/contribute/general-guidelines.md | 228 ++++++++++++++---- src/docs/contribute/testing-guidelines.md | 52 +--- tests/playwright/env/parseDotEnvFile.ts | 2 +- 30 files changed, 788 insertions(+), 169 deletions(-) create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .env.playwright.dist create mode 100644 docker-compose.base.yml delete mode 100644 docker-compose.yml create mode 100644 docker-compose.yml.dist create mode 100644 docker/build-docker-images.sh create mode 100644 docker/react_ui_devcontainer/Dockerfile create mode 100644 docker/react_ui_devcontainer/files/home/developer/shell-init.fish create mode 100644 docker/react_ui_devcontainer/files/home/developer/shell-init.sh create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/claude create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/copilot create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/docker-entrypoint create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/mkdocs create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/node create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/npm create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/npx create mode 100644 docker/react_ui_devcontainer/files/usr/local/bin/opencode create mode 100644 docker/react_ui_devcontainer_local/Dockerfile.dist create mode 100644 scripts/auto-start-mkdocs.sh create mode 100644 scripts/auto-start-node.sh create mode 100755 scripts/write-lockfile-hash.sh create mode 100755 setup.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..3a1fa1795 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "React UI (${localWorkspaceFolderBasename})", + "initializeCommand": "bash ./setup.sh", + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "devcontainer", + "shutdownAction": "stopCompose", + "workspaceFolder": "/workspace", + "forwardPorts": [ + "docs:8000" + ], + "portsAttributes": { + "docs:8000": { + "label": "Docs server", + "protocol": "http", + "onAutoForward": "openBrowser" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + } +} diff --git a/.env.dist b/.env.dist index d60e62cef..683f6be58 100644 --- a/.env.dist +++ b/.env.dist @@ -1,13 +1,51 @@ -############################### -# Docker compose configuration # -############################### +################################ +# Docker Compose configuration # +################################ -# Host system port where the live documentation is to be made accessible -COMPOSE_START_PORT=8000 +# Must match Docker Compose project name used to start the other service containers to allow devcontainer +# to communicate with them (setup.sh derives this from the directory basename by default) +COMPOSE_PROJECT_NAME=react-ui -# Host system port where Playwright Component Testing report is to be made accessible +# Docker compose ports for Docs server instances +COMPOSE_DOCS_SERVER_PORT=8000 + +# Docker compose ports for Playwright Component Testing report server COMPOSE_PLAYWRIGHT_REPORT_PORT=9323 +# Flag whether the `node` and `docs` service containers should automatically install dependencies, +# build, and run the application (JavaScript files watcher, docs server) when they start +COMPOSE_AUTOSTART=false + # Ownership of the files created in the container +# ⚠️ [Linux] This needs to be set to the output of `id --user` +# ⚠️ [MacOS] This needs to be set to 1000 COMPOSE_UID=1000 +# ⚠️ [Linux] This needs to be set to the output of `id --group` +# ⚠️ [MacOS] This needs to be set to 1000 COMPOSE_GID=1000 + +############################# +# Devcontainer configuration # +############################# + +# IDEs automatically mount the host's SSH agent socket into the container +# Visual Studio Code does this by default, it can be disabled by setting the following variable to true. +# JetBrains IDEs do not mount this by default, but they can be configured to do so. +BLOCK_SSH_AUTH_SOCK=false + +# Select your preferred editor and visual (vim, nano) +EDITOR=vim +VISUAL=vim + +# Select your preferred shell (/bin/bash, /bin/fish, /bin/zsh) +SHELL=/bin/bash + +########################### +# Playwright configuration # +########################### + +# Number of workers to use to run Playwright tests +PW_WORKERS=1 + +# Port used by Playwright Component Testing to serve the test files +PW_CT_PORT=3100 diff --git a/.env.playwright.dist b/.env.playwright.dist deleted file mode 100644 index 41bd68e69..000000000 --- a/.env.playwright.dist +++ /dev/null @@ -1,9 +0,0 @@ -########################### -# Playwright configuration # -########################### - -# Number of workers to use to run Playwright tests -PW_WORKERS=1 - -# Port used by Playwright Component Testing to serve the test files -PW_CT_PORT=3100 diff --git a/.gitignore b/.gitignore index 2b0232bac..e2ab471d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /coverage +/docker-compose.yml +/docker/react_ui_devcontainer_local +!/docker/react_ui_devcontainer_local/Dockerfile.dist /dist /node_modules /playwright-report/ @@ -6,6 +9,5 @@ /src/docs/_assets/generated/* /tests/playwright/.temp/ .env -.env.playwright statistics.html !.gitkeep diff --git a/CLAUDE.md b/CLAUDE.md index ebcbc7f3f..65b606a1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,17 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands -All `npm` commands must be run inside Docker containers. Use `node_shell` for most tasks, `playwright` for visual tests. +All commands below are meant to be run directly inside the Docker container `devcontainer`. +If you open the project in a Dev Container, you can run these commands without manually +starting Docker Compose on the host. -```bash -# Enter node_shell container -docker compose run --rm node_shell - -# Enter playwright container (for visual tests) -docker compose run --rm --service-ports playwright -``` - -**Within `node_shell`:** ```bash npm run lint # All linters (ESLint + Stylelint + Markdownlint) @@ -23,11 +16,6 @@ npm run stylelint # SCSS linting npm run test:jest # All Jest unit tests npm run test:jest:ts -- # Single TypeScript test file npm run test:jest:js -- # Single JavaScript test file -``` - -**Within `playwright`:** - -```bash npm run test:playwright-ct:all # All component tests npm run test:playwright-ct:all-with-update # Update snapshots npm run test:playwright-ct:all -- -- src/components/Button # Tests for one component diff --git a/docker-compose.base.yml b/docker-compose.base.yml new file mode 100644 index 000000000..41d225068 --- /dev/null +++ b/docker-compose.base.yml @@ -0,0 +1,93 @@ +services: + # This service is responsible for providing the main development environment for developers + devcontainer: + hostname: ${COMPOSE_PROJECT_NAME:-react-ui}_devcontainer + build: + context: docker/react_ui_devcontainer/ + dockerfile: Dockerfile + # Start dependent services before starting the `devcontainer` service to ensure that the necessary environments + # and tools are available when the `devcontainer` starts. + depends_on: + node: + condition: service_started + playwright: + condition: service_started + docs: + condition: service_started + # Run as host UID/GID so files created in mounted volumes are owned correctly on the host. + # The images use `fixuid` (https://github.com/boxboat/fixuid) to remap the built-in `developer` + # user to these IDs at startup — the `user:` directive is required for that remap to work. + # Applied to all services below for the same reason. + user: ${COMPOSE_UID}:${COMPOSE_GID} + # Keep the container running indefinitely to allow developers to attach to it and use it as their development environment + command: sleep infinity + # Injects environment variables from the `.env` file into the `devcontainer` service, + # making them accessible within the container's environment. + env_file: + - .env + environment: + # This must be set correctly for the `devcontainer` to be able to access the host's Docker daemon, + # enabling Docker-from-Docker capabilities (e.g., running Docker commands from within the `devcontainer`). + COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-react-ui} + init: true + volumes: + - .:/workspace:z + # The following volume is used to allow the `devcontainer` to access the host's Docker daemon, + # enabling Docker-from-Docker capabilities (e.g., running Docker commands from within the `devcontainer`). + - /var/run/docker.sock:/var/run/docker.sock + # The following named volumes persist data (e.g. terminal history, AI tools data, etc.) across container restarts. + # Using separate named volumes (instead of a single volume with subpaths) allows Docker to automatically + # seed the volume with data from the image on first use. + - terminal-history:/home/developer/.terminal_history + - claude-config:/home/developer/.config/claude + - claude-state:/home/developer/.local/state/claude + - copilot:/home/developer/.copilot + - copilot-config:/home/developer/.config/copilot + - opencode-config:/home/developer/.config/opencode + - opencode-share:/home/developer/.local/share/opencode + - opencode-state:/home/developer/.local/state/opencode + + # This service provides Node environment and NPM + node: + build: docker/node + user: ${COMPOSE_UID}:${COMPOSE_GID} + entrypoint: sh -c 'if [ "$$COMPOSE_AUTOSTART" = "true" ]; then sh scripts/auto-start-node.sh; else sleep infinity; fi' + env_file: + - .env + volumes: + - .:/workspace:z + + # This service provides Playwright environment and tools for browser automation and testing + playwright: + build: docker/playwright + user: ${COMPOSE_UID}:${COMPOSE_GID} + command: sleep infinity + env_file: + - .env + ports: + - ${COMPOSE_PLAYWRIGHT_REPORT_PORT}:9323 + volumes: + - .:/workspace:z + + # This provides server for documentation + docs: + build: docker/mkdocs + user: ${COMPOSE_UID}:${COMPOSE_GID} + entrypoint: sh -c 'if [ "$$COMPOSE_AUTOSTART" = "true" ]; then sh scripts/auto-start-mkdocs.sh; else sleep infinity; fi' + env_file: + - .env + ports: + - ${COMPOSE_DOCS_SERVER_PORT}:8000 + volumes: + - .:/workspace:z + +volumes: + # The following volumes are used to persist data (e.g. terminal history, AI tools data, etc.) across container restarts + terminal-history: + claude-config: + claude-state: + copilot: + copilot-config: + opencode-config: + opencode-share: + opencode-state: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 6896b094f..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -services: - # Base services - do not run directly - mkdocs: - build: docker/mkdocs - user: ${COMPOSE_UID-1000}:${COMPOSE_GID-1000} - volumes: - - .:/workspace:z - node: - build: docker/node - user: ${COMPOSE_UID-1000}:${COMPOSE_GID-1000} - volumes: - - .:/workspace:z - - # Dev services - mkdocs_dev_server: - extends: mkdocs - entrypoint: mkdocs serve - ports: - - ${COMPOSE_START_PORT-8000}:8000 - node_dev_server: - extends: node - entrypoint: npm start - node_shell: - extends: node - entrypoint: bash - - # For running Playwright tests - playwright: - build: docker/playwright - entrypoint: bash - user: ${COMPOSE_UID-1000}:${COMPOSE_GID-1000} - ports: - - ${COMPOSE_PLAYWRIGHT_REPORT_PORT-9323}:9323 - volumes: - - .:/workspace:z - - # Build services - mkdocs_build_site: - extends: mkdocs - entrypoint: mkdocs build - node_build_site: - extends: node - entrypoint: npm run build diff --git a/docker-compose.yml.dist b/docker-compose.yml.dist new file mode 100644 index 000000000..1fa6346a1 --- /dev/null +++ b/docker-compose.yml.dist @@ -0,0 +1,41 @@ +services: + # This service is responsible for providing the main development environment for developers + devcontainer: + extends: + file: docker-compose.base.yml + service: devcontainer + # Use `build` when you want to customize the devcontainer using `docker/react_ui_devcontainer_local/Dockerfile` + # build: + # context: ./docker/react_ui_devcontainer_local/ + # dockerfile: Dockerfile + # Use `image` when you want to use the default devcontainer + image: react-ui_devcontainer + + # This service provides Node environment and NPM + node: + extends: + file: docker-compose.base.yml + service: node + + # This service provides Playwright environment and tools for browser automation and testing + playwright: + extends: + file: docker-compose.base.yml + service: playwright + + # This provides server for documentation + docs: + extends: + file: docker-compose.base.yml + service: docs + +volumes: + # The following volumes are used to persist data (e.g. terminal history, AI tools data, etc.) across container restarts + terminal-history: + claude-config: + claude-state: + copilot: + copilot-config: + opencode-config: + opencode-share: + opencode-state: diff --git a/docker/build-docker-images.sh b/docker/build-docker-images.sh new file mode 100644 index 000000000..03ad3368e --- /dev/null +++ b/docker/build-docker-images.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e +trap 'echo "Failed to build Docker images"; exit 1' ERR + +cd "$(dirname "$0")" + +echo "Building Docker images..." + +if [ ! -f ../.env ]; then + echo "Error: .env file not found in the project root" + exit 1 +fi + +PROJECT_NAME=$(grep -E '^COMPOSE_PROJECT_NAME=' ../.env | cut -d '=' -f 2-) +PROJECT_DEVCONTAINER_IMAGE="${PROJECT_NAME}_devcontainer" + +echo "Building Docker image $PROJECT_DEVCONTAINER_IMAGE..." +docker build -t "$PROJECT_DEVCONTAINER_IMAGE" -f ./react_ui_devcontainer/Dockerfile ./react_ui_devcontainer/ + +cd .. + +echo "Building project Docker images using docker-compose..." +docker compose build + +echo "All Docker images built successfully!" diff --git a/docker/mkdocs/Dockerfile b/docker/mkdocs/Dockerfile index 77d051bda..102a1285d 100644 --- a/docker/mkdocs/Dockerfile +++ b/docker/mkdocs/Dockerfile @@ -1,3 +1,5 @@ -FROM squidfunk/mkdocs-material:9 +# We freezed the version of mkdocs-material to prevent issue with live reload +# See +FROM squidfunk/mkdocs-material:9.6.20 RUN mkdir /workspace WORKDIR /workspace diff --git a/docker/react_ui_devcontainer/Dockerfile b/docker/react_ui_devcontainer/Dockerfile new file mode 100644 index 000000000..b3f48f14b --- /dev/null +++ b/docker/react_ui_devcontainer/Dockerfile @@ -0,0 +1,129 @@ +FROM debian:bookworm-slim AS react-ui-devcontainer + +# Build arguments for user configuration +# Those can be changed using fixuid's remapping feature when running the container +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 +ARG HOME="/home/${USERNAME}" + +# Default shell and editor settings, overridable via .env +ENV EDITOR="vim" +ENV VISUAL="vim" +ENV SHELL="/bin/bash" +ENV CLAUDE_CONFIG_DIR="${HOME}/.config/claude" +ENV OPENCODE_CONFIG="${HOME}/.config/opencode/opencode.json" +ENV OPENCODE_CONFIG_DIR="${HOME}/.config/opencode" + +# Install sudo, editors (vim, nano), SSH client, Git, shells (zsh, fish) and common utilities +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + ca-certificates \ + curl \ + gnupg \ + vim \ + nano \ + openssh-client \ + git \ + fish \ + zsh && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create the user +RUN addgroup --gid $USER_GID $USERNAME && \ + adduser --uid $USER_UID --ingroup $USERNAME --home $HOME --shell $SHELL --disabled-password --gecos "" $USERNAME && \ + echo "$USERNAME ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \ + chmod 0440 /etc/sudoers.d/$USERNAME + +# Install fixuid to allow remapping the `developer` user to the host UID/GID when running in a container +RUN USER=$USERNAME && \ + GROUP=$USERNAME && \ + ARCH=$(dpkg --print-architecture) && \ + curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-${ARCH}.tar.gz | tar -C /usr/local/bin -xzf - && \ + chown root:root /usr/local/bin/fixuid && \ + chmod 4755 /usr/local/bin/fixuid && \ + mkdir -p /etc/fixuid && \ + printf "user: $USER\ngroup: $GROUP\npaths: [$HOME]" > /etc/fixuid/config.yml + +# Install Docker CLI and Compose plugin to enable Docker-from-Docker +RUN install -m 0755 -d /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg && \ + chmod a+r /etc/apt/keyrings/docker.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ + > /etc/apt/sources.list.d/docker.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Switch to the `developer` user for all user-level installations +USER $USERNAME:$USERNAME + +# Install Oh My Zsh +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended + +# Persist terminal history across container restarts via a Docker volume +# mounted at $HOME/.terminal_history in docker-compose.base.yml +RUN mkdir -p "$HOME/.terminal_history" && \ + touch "$HOME/.terminal_history/bash_history" && \ + touch "$HOME/.terminal_history/zsh_history" && \ + touch "$HOME/.terminal_history/fish_history" && \ + SNIPPET_BASH="export PROMPT_COMMAND='history -a' && export HISTFILE=$HOME/.terminal_history/bash_history" && \ + echo "$SNIPPET_BASH" >> "$HOME/.bashrc" && \ + SNIPPET_ZSH="export PROMPT_COMMAND='history -a' && export HISTFILE=$HOME/.terminal_history/zsh_history" && \ + echo "$SNIPPET_ZSH" >> "$HOME/.zshrc" && \ + mkdir -p "$HOME/.local/share/fish" && \ + touch "$HOME/.terminal_history/fish_history" && \ + ln -sf "$HOME/.terminal_history/fish_history" "$HOME/.local/share/fish/fish_history" + +# Install Claude Code CLI +RUN mkdir -p "$HOME/.config/claude" && \ + curl -fsSL https://claude.ai/install.sh | bash + +# Install Open Code CLI +RUN curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path + +# Install GitHub Copilot CLI +# Note: Copilot CLI does not support XDG configuration, see: +# * +# * +RUN curl -fsSL https://gh.io/copilot-install | PREFIX="$HOME/.local" bash + +# Wrapper scripts for AI coding assistants ensure they are available in PATH +# and handle necessary environment variable setup +COPY --chmod=+x files/usr/local/bin/copilot /usr/local/bin/copilot +COPY --chmod=+x files/usr/local/bin/claude /usr/local/bin/claude +COPY --chmod=+x files/usr/local/bin/opencode /usr/local/bin/opencode +COPY --chmod=+x files/usr/local/bin/docker-entrypoint /usr/local/bin/docker-entrypoint +COPY --chmod=+x files/home/$USERNAME/shell-init.sh ${HOME}/shell-init.sh +COPY --chmod=+x files/home/$USERNAME/shell-init.fish ${HOME}/shell-init.fish + +# Wrapper scripts that execute commands inside the appropriate Docker containers. +# These scripts use Docker-from-Docker to run the commands in the context of the +# respective Docker containers, allowing container agnostic development workflows. +COPY --chmod=+x files/usr/local/bin/node /usr/local/bin/node +COPY --chmod=+x files/usr/local/bin/npm /usr/local/bin/npm +COPY --chmod=+x files/usr/local/bin/npx /usr/local/bin/npx +COPY --chmod=+x files/usr/local/bin/mkdocs /usr/local/bin/mkdocs + +# Ensure /usr/local/bin takes precedence over VS Code injected paths +# and run profile script on every shell startup to apply .env configuration +RUN mkdir -p $HOME/.config/fish && \ + echo 'export PATH="/usr/local/bin:$PATH"' >> $HOME/.bashrc && \ + echo 'export PATH="/usr/local/bin:$PATH"' >> $HOME/.zshrc && \ + echo 'set -gx PATH /usr/local/bin $PATH' >> $HOME/.config/fish/config.fish && \ + echo '. $HOME/shell-init.sh' >> $HOME/.bashrc && \ + echo '. $HOME/shell-init.sh' >> $HOME/.zshrc && \ + echo 'source $HOME/shell-init.fish' >> $HOME/.config/fish/config.fish + +# Set the default working directory when starting a container from this image +WORKDIR /workspace + +# Set the default command to run when starting a container from this image +# The fixuid command will adjust the `developer` user's UID and GID to match +# the host user's UID and GID when running in a container, ensuring that +# any files created by the `developer` user inside the container have +# the correct ownership on the host system. +ENTRYPOINT ["docker-entrypoint"] diff --git a/docker/react_ui_devcontainer/files/home/developer/shell-init.fish b/docker/react_ui_devcontainer/files/home/developer/shell-init.fish new file mode 100644 index 000000000..e7d6a7803 --- /dev/null +++ b/docker/react_ui_devcontainer/files/home/developer/shell-init.fish @@ -0,0 +1,16 @@ +#!/usr/bin/env fish + +# Block SSH agent forwarding if the environment variable `BLOCK_SSH_AUTH_SOCK` is set to "true" +# +# VS Code forwards the SSH agent socket to the dev container by default, so this allows users +# to opt out of that behavior if they want to disable SSH agent forwarding to isolate the dev container. +# JetBrains IDEs forward the SSH agent socket only if allowed by the user. +if test "$BLOCK_SSH_AUTH_SOCK" = "true" + # Erase any existing SSH_AUTH_SOCK from all scopes (local, global, universal) + # so the subsequent `set -gx` is not shadowed by a pre-existing value. + set -e SSH_AUTH_SOCK + # Disable SSH agent forwarding for the current shell session + set -gx SSH_AUTH_SOCK /dev/null + # Remove any existing SSH agent socket files created by VS Code + find /tmp -maxdepth 1 -name 'vscode-ssh-auth-*.sock' -delete 2>/dev/null +end diff --git a/docker/react_ui_devcontainer/files/home/developer/shell-init.sh b/docker/react_ui_devcontainer/files/home/developer/shell-init.sh new file mode 100644 index 000000000..91669c995 --- /dev/null +++ b/docker/react_ui_devcontainer/files/home/developer/shell-init.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# Block SSH agent forwarding if the environment variable `BLOCK_SSH_AUTH_SOCK` is set to "true" +# +# VS Code forwards the SSH agent socket to the dev container by default, so this allows users +# to opt out of that behavior if they want to disable SSH agent forwarding to isolate the dev container. +# JetBrains IDEs forward the SSH agent socket only if allowed by the user. +if [ "$BLOCK_SSH_AUTH_SOCK" = "true" ]; then + # Disable SSH agent forwarding for the current shell session + export SSH_AUTH_SOCK=/dev/null + # Remove any existing SSH agent socket files created by VS Code + find /tmp -maxdepth 1 -name 'vscode-ssh-auth-*.sock' -delete 2>/dev/null +fi diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/claude b/docker/react_ui_devcontainer/files/usr/local/bin/claude new file mode 100644 index 000000000..de603a311 --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/claude @@ -0,0 +1,3 @@ +#!/bin/sh + +/home/developer/.local/bin/claude "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/copilot b/docker/react_ui_devcontainer/files/usr/local/bin/copilot new file mode 100644 index 000000000..4df0dff87 --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/copilot @@ -0,0 +1,3 @@ +#!/bin/sh + +/home/developer/.local/bin/copilot --config-dir "/home/developer/.config/copilot" "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/docker-entrypoint b/docker/react_ui_devcontainer/files/usr/local/bin/docker-entrypoint new file mode 100644 index 000000000..661ebd04f --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/docker-entrypoint @@ -0,0 +1,10 @@ +#!/bin/sh + +# Ensure the developer user owns their home directory and all files within it +sudo chown -R developer:developer /home/developer + +# Remap the developer user's UID/GID to match the host user +eval "$(fixuid -q)" + +# Execute the command passed to the container (e.g. via `command` in docker-compose) +exec "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/mkdocs b/docker/react_ui_devcontainer/files/usr/local/bin/mkdocs new file mode 100644 index 000000000..3aa684f0c --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/mkdocs @@ -0,0 +1,5 @@ +#!/bin/sh + +# This script is a wrapper around the `mkdocs` command that executes it within the Docker container. +# Docker Socket is owned by root, so we need to use `sudo` to access it. +sudo docker compose -p "$COMPOSE_PROJECT_NAME" exec docs mkdocs "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/node b/docker/react_ui_devcontainer/files/usr/local/bin/node new file mode 100644 index 000000000..e96ed06c1 --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/node @@ -0,0 +1,5 @@ +#!/bin/sh + +# This script is a wrapper around the `node` command that executes it within the Docker container. +# Docker Socket is owned by root, so we need to use `sudo` to access it. +sudo docker compose -p "$COMPOSE_PROJECT_NAME" exec node node "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/npm b/docker/react_ui_devcontainer/files/usr/local/bin/npm new file mode 100644 index 000000000..57479ef1e --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/npm @@ -0,0 +1,9 @@ +#!/bin/sh + +# This script is a wrapper around the `npm` command that executes it within the Docker container. +# Docker Socket is owned by root, so we need to use `sudo` to access it. +if [ "$1" = "run" ] && echo "$2" | grep -q '^test:playwright'; then + sudo docker compose -p "$COMPOSE_PROJECT_NAME" exec playwright npm "$@" +else + sudo docker compose -p "$COMPOSE_PROJECT_NAME" exec node npm "$@" +fi diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/npx b/docker/react_ui_devcontainer/files/usr/local/bin/npx new file mode 100644 index 000000000..a204fd8d4 --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/npx @@ -0,0 +1,5 @@ +#!/bin/sh + +# This script is a wrapper around the `npx` command that executes it within the Docker container. +# Docker Socket is owned by root, so we need to use `sudo` to access it. +sudo docker compose -p "$COMPOSE_PROJECT_NAME" exec node npx "$@" diff --git a/docker/react_ui_devcontainer/files/usr/local/bin/opencode b/docker/react_ui_devcontainer/files/usr/local/bin/opencode new file mode 100644 index 000000000..17c2198ee --- /dev/null +++ b/docker/react_ui_devcontainer/files/usr/local/bin/opencode @@ -0,0 +1,3 @@ +#!/bin/sh + +/home/developer/.opencode/bin/opencode "$@" diff --git a/docker/react_ui_devcontainer_local/Dockerfile.dist b/docker/react_ui_devcontainer_local/Dockerfile.dist new file mode 100644 index 000000000..b61d6a879 --- /dev/null +++ b/docker/react_ui_devcontainer_local/Dockerfile.dist @@ -0,0 +1,4 @@ +# Image name `react-ui_devcontainer` might differ based on Docker compose project name +FROM react-ui_devcontainer as react-ui_devcontainer_local + +# Your additional setup for the local development can go here diff --git a/package.json b/package.json index 4a3cb3758..8ff2eb70d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "lint": "npm run eslint && npm run markdownlint && npm run stylelint", "markdownlint": "markdownlint-cli2 \"README.md\" \"src/**/*.md\"", "postbuild": "npm run copy", - "postinstall": "cp -n .env.dist .env && cp -n .env.playwright.dist .env.playwright || true", + "postinstall": "sh scripts/write-lockfile-hash.sh", "precopy": "rm -rf dist && mkdir dist", "prepublishOnly": "npm run build", "start": "webpack --watch --mode=development", diff --git a/scripts/auto-start-mkdocs.sh b/scripts/auto-start-mkdocs.sh new file mode 100644 index 000000000..680c38c84 --- /dev/null +++ b/scripts/auto-start-mkdocs.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# Start the docs server +echo "Starting the docs server..." +mkdocs serve diff --git a/scripts/auto-start-node.sh b/scripts/auto-start-node.sh new file mode 100644 index 000000000..50a11de5d --- /dev/null +++ b/scripts/auto-start-node.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -e + +# File to read the hash of the package-lock.json +LOCK_HASH_FILE="node_modules/.package-lock-hash" + +# Parent directory of the script +SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" + +# Change to the parent directory of the script +cd "$SCRIPT_DIR/.." + +# Install dependencies if node_modules is missing or out of date +CURRENT_HASH=$(sha256sum package-lock.json | awk '{print $1}') +if [ ! -d "node_modules" ]; then + echo "Installing dependencies (node_modules directory is missing)..." + npm ci +elif [ ! -f "$LOCK_HASH_FILE" ]; then + echo "Installing dependencies (lockfile of package-lock.json is missing)..." + npm ci +elif [ "$(cat "$LOCK_HASH_FILE")" != "$CURRENT_HASH" ]; then + echo "Installing dependencies (package-lock.json has changed)..." + npm ci +fi + +# Build the application (must be run prior to starting the server to ensure the latest code is used) +echo "Building the application..." +npm run build + +# Start the application +echo "Starting the application..." +npm start diff --git a/scripts/write-lockfile-hash.sh b/scripts/write-lockfile-hash.sh new file mode 100755 index 000000000..6b63f2f62 --- /dev/null +++ b/scripts/write-lockfile-hash.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +# File to store the hash of the package-lock.json +LOCK_HASH_FILE="node_modules/.package-lock-hash" + +# Parent directory of the script +SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" + +# Change to the parent directory of the script +cd "$SCRIPT_DIR/.." + +# Record the hash of the lockfile we just installed against +sha256sum package-lock.json | awk '{print $1}' > "$LOCK_HASH_FILE" diff --git a/setup.sh b/setup.sh new file mode 100755 index 000000000..9422d7989 --- /dev/null +++ b/setup.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +DEFAULT_PROJECT_NAME="react-ui" +DEFAULT_USER_ID=1000 +DEFAULT_GROUP_ID=1000 + +set -e +trap 'echo "Failed to set up project"; rm -f .env.temp; exit 1' ERR + +# Function to handle sed command with cross-platform compatibility +sed_cmd() { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +cd "$(dirname "$0")" + +echo "Setting up project..." + +# Create and configure .env file if it doesn't exist +if [ ! -f .env ]; then + echo "Creating .env file..." + cp .env.dist .env.temp + + echo "Configuring .env file..." + + PROJECT_PATH=$(pwd) + PROJECT_NAME=$(basename "$PROJECT_PATH") + + if [[ "$OSTYPE" == "darwin"* ]]; then + # MacOS + USER_ID=$DEFAULT_USER_ID + GROUP_ID=$DEFAULT_GROUP_ID + else + # Linux + USER_ID=$(id -u) + GROUP_ID=$(id -g) + fi + + sed_cmd "s|^COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=$PROJECT_NAME|" .env.temp + sed_cmd "s|^COMPOSE_UID=.*|COMPOSE_UID=$USER_ID|" .env.temp + sed_cmd "s|^COMPOSE_GID=.*|COMPOSE_GID=$GROUP_ID|" .env.temp + + LOCAL_SHELL= + if [ -n "$SHELL" ]; then + sed_cmd "s|^SHELL=.*|SHELL=$SHELL|" .env.temp + LOCAL_SHELL=$SHELL + fi + + LOCAL_EDITOR= + if [ -n "$EDITOR" ]; then + sed_cmd "s|^EDITOR=.*|EDITOR=$EDITOR|" .env.temp + LOCAL_EDITOR=$EDITOR + fi + + LOCAL_VISUAL= + if [ -n "$VISUAL" ]; then + sed_cmd "s|^VISUAL=.*|VISUAL=$VISUAL|" .env.temp + LOCAL_VISUAL=$VISUAL + fi + + cp .env.temp .env + rm .env.temp + + echo "Configured .env file with the following values:" + echo "Project name: $PROJECT_NAME" + echo "Project path: $PROJECT_PATH" + echo "User ID: $USER_ID" + echo "Group ID: $GROUP_ID" + + if [ -n "$LOCAL_SHELL" ]; then + echo "Shell: $LOCAL_SHELL" + fi + if [ -n "$LOCAL_EDITOR" ]; then + echo "Editor: $LOCAL_EDITOR" + fi + if [ -n "$LOCAL_VISUAL" ]; then + echo "Visual: $LOCAL_VISUAL" + fi +else + echo ".env file already exists, skipping creation." +fi + +# Create docker-compose.yml if it doesn't exist +if [ ! -f docker-compose.yml ]; then + echo "Creating and configuring docker-compose.yml file..." + cp docker-compose.yml.dist docker-compose.yml + + DEFAULT_PROJECT_DEVCONTAINER_IMAGE="${DEFAULT_PROJECT_NAME}_devcontainer" + PROJECT_NAME=$(grep -E '^COMPOSE_PROJECT_NAME=' .env | cut -d '=' -f 2-) + PROJECT_DEVCONTAINER_IMAGE="${PROJECT_NAME}_devcontainer" + + sed_cmd "s|image: $DEFAULT_PROJECT_DEVCONTAINER_IMAGE|image: $PROJECT_DEVCONTAINER_IMAGE|" docker-compose.yml +else + echo "docker-compose.yml file already exists, skipping creation." +fi + +# Build Docker images +sh ./docker/build-docker-images.sh + +echo "Project setup completed successfully!" diff --git a/src/docs/contribute/general-guidelines.md b/src/docs/contribute/general-guidelines.md index 74aa840ac..c30d615ae 100644 --- a/src/docs/contribute/general-guidelines.md +++ b/src/docs/contribute/general-guidelines.md @@ -2,99 +2,232 @@ In the first place, thank you for your interest in contributing! 🙏 -## Development +## Development Environment -Working on the site requires: +### Overview -* [Docker] -* [Docker Compose] +All development is done inside the container named `devcontainer` which +contains all necessary tools and dependencies. All commands in the documentation +are container-agnostic and are meant to be run directly inside the `devcontainer`. -This allows running the documentation site which serves as a development platform. +The `devcontainer` orchestrates other service containers behind the scenes via +Docker-from-Docker. Other containers are implementation details and should not +be accessed directly. -### Configure Docker Compose +There are two supported ways to access the development environment. The +recommended way is to use [Development Containers] with an IDE, which provides +a more seamless experience. The alternative is to use Docker Compose directly, +which is suitable for cases when IDE integration is not needed +(e.g. terminal-only workflows). -Review the default env variable values in the `docker-compose.yml` file. -The defaults should work for most systems, but can be changed if needed. -To change them, edit the `.env` file as needed. +### Requirements -### Use Docker Compose - -#### Node shell +* [Docker] +* [Docker Compose] +* [Development Containers] (Are recommended) -All npm commands such as `npm ci`, `npm test`, `npm run eslint` and others you -need to run them within the `node_shell` Docker container. +### Setup -To log into the container, run: +#### Automatic setup -```bash -docker compose run --rm node_shell -``` +> You can skip this section when using [Development Containers]. They run this +> script automatically the first time the project is opened. -If you want to run single command, run: +Run the setup script to automatically create and configure all necessary files +and build Docker images: ```bash -docker compose run --rm node_shell -c 'npm ' +bash setup.sh ``` -#### Run the Dev Server +#### Manual setup + +If you prefer to set up the project manually: -1. **Within `node_shell`:** Install dependencies: +1. Create `.env` file and configure it: ```bash - npm ci + cp .env.dist .env ``` -2. **On host:** Run development server: +2. Create `docker-compose.yml` and configure it: ```bash - docker compose up node_dev_server mkdocs_dev_server + cp docker-compose.yml.dist docker-compose.yml ``` -#### Build the Project - -1. **On host:** Make sure the dev server is not running: +3. Build Docker images: ```bash - docker compose down + bash docker/build-docker-images.sh ``` -2. **Within `node_shell`:** Install dependencies: +#### Environment + +The `.env` file configures services (ports, UID/GID, source mapping), +the `devcontainer` shell, editor and SSH agent forwarding, as well as application +settings. See `.env.dist` for available options. + +### Accessing the Development Environment + +#### Using Development Containers + +Open the project in an IDE that supports [Development Containers] (e.g. +[Visual Studio Code][vscode-devcontainers], [JetBrains IDEs][jetbrains-devcontainers]). +The IDE will automatically set up the environment using the configuration in +`.devcontainer/devcontainer.json`. + +#### Using Docker Compose + +1. Start the `devcontainer` in the background: ```bash - npm ci + docker compose up -d ``` -3. **On host:** Build JS: +2. Open a shell inside the `devcontainer`: ```bash - docker compose run --rm node_build_site + docker compose exec devcontainer bash ``` -4. **On host:** Build mkDocs: +3. To stop the environment: ```bash - docker compose run --rm mkdocs_build_site + docker compose down ``` -#### Playwright +### Customization -npm commands such as `test:playwright-ct:all` and `test:playwright-ct:all-with-update` -need to be run them within the `playwright` Docker container. +To customize the `devcontainer`, create a +`docker/react_ui_devcontainer_local/Dockerfile` that extends the base image: -To log into the container, run: +```Dockerfile +# Image name `react-ui_devcontainer` might differ based on Docker compose project name +FROM react-ui_devcontainer as react-ui_devcontainer_local +# Add your customizations here +``` + +Then ensure `docker-compose.yml` has the `build` directive for the `devcontainer` +service: + +```yml +devcontainer: + extends: + file: docker-compose.base.yml + service: devcontainer + build: + context: ./docker/react_ui_devcontainer_local/ + dockerfile: Dockerfile +``` + +Rebuild the images after making changes: ```bash -docker compose run --rm --service-ports playwright +bash docker/build-docker-images.sh ``` -If you want to run single command, run: +If you need to persist additional data across container restarts, see how it is +done in `docker-compose.base.yml`. You will need to add a volume mapping to the +`devcontainer` service and add a corresponding named volume definition. + +### What the `devcontainer` Contains + +The `devcontainer` is built in the following layers: + +#### Base Layer (`react-ui_devcontainer`) + +General-purpose development layer. Makes the environment container-agnostic +by wrapping commands to run in the appropriate service containers. + +* **OS:** Debian Bookworm +* **Shells:** Bash, Zsh (with Oh My Zsh), Fish +* **Editors:** Vim, Nano +* **Tools:** Git, SSH client, Docker CLI (Docker-from-Docker) +* **AI coding assistants:** Claude Code, GitHub Copilot CLI, Open Code + +#### Local Layer (`react-ui_devcontainer_local`) + +Optional layer that allows individual developers to customize the environment. +See [Customization](#customization) for details. + +### Service Containers + +The `devcontainer` depends on the following service containers defined in +`docker-compose.base.yml`: + +| Container | Purpose | +|--------------|--------------------------------------------------| +| `node` | Runs Node.js commands (`npm`, `node`) | +| `playwright` | Runs Playwright and Lighthouse tests | +| `docs` | Serves documentation via MkDocs | + +All service containers mount the workspace at `/workspace` so that file changes +are shared. + +## Automatic Service Bootstrap + +> You can skip this section if you do not want to automatically +> [install dependencies](#installing-dependencies), [build](#building), and +> [run](#running) the application, or if you are not an experienced developer. +> If you do use it, you can skip those sections as well, since the steps they +> describe are performed automatically. + +Setting `COMPOSE_AUTOSTART=true` in `.env` makes the `node` and `docs` +service containers automatically install dependencies, build, and run the +application when they start. The default is `false`. + +Setting `COMPOSE_AUTOSTART=true` comes with the following trade-offs: + +* **Changes to dependencies require a container restart.** The watcher owns the + service container's entrypoint, so updating dependencies (e.g. pulling a + branch that changes `package-lock.json`, or running `npm install `) + only takes effect after restarting the `node` service container. The same + applies to changes that affect the documentation server. +* **Service logs are not directly visible.** The watcher and docs server run in + their own service containers rather than in your `devcontainer` shell, so + their output is not shown alongside your regular terminal work. You have to + inspect it via `docker compose logs ` from the host. + +> If something is not working as expected, or you are not sure what is going on, +> set `COMPOSE_AUTOSTART=false`, restart the containers, and follow the +> manual steps in the sections below instead. + +## Installing Dependencies + +Run it on initial setup or when dependencies have changed: + +```bash +npm ci +``` + +## Building + +To build the JavaScript code: + +```bash +npm run build +``` + +To build the documentation: + +```bash +mkdocs build +``` + +## Running + +To start building JavaScript files in watch mode: ```bash -docker compose run --rm --service-ports playwright -c 'npm run test:playwright-ct:*' +npm start ``` -Argument `--service-ports` is used to expose the ports of the container to the host -to serve the test report. +To start the documentation server: + +```bash +mkdocs serve +``` ## Testing @@ -161,7 +294,7 @@ pull request from the changelog. The best way for development of React UI is to link `react-ui` into your application with `npm link` so you can see it in action. -1. In React UI repository, run `npm link` +1. In React UI repository **on your host machine**, run `npm link` 2. In your application, run `npm link @react-ui-org/react-ui` To prevent [Invalid Hook Call Warning][react-invalid-hook] when React UI is @@ -202,8 +335,11 @@ the documentation platform. Do see their respective documentation for details. -[Docker]: https://www.docker.com +[Development Containers]: https://containers.dev/ +[Docker]: https://docs.docker.com/get-started/ [Docker Compose]: https://docs.docker.com/compose/ +[jetbrains-devcontainers]: https://www.jetbrains.com/help/idea/start-dev-container-inside-ide.html#dev_container_context_menu +[vscode-devcontainers]: https://code.visualstudio.com/docs/devcontainers/tutorial [react-invalid-hook]: https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react [mkdocs-material]: https://squidfunk.github.io/mkdocs-material/ [Docoff]: https://github.com/react-ui-org/docoff diff --git a/src/docs/contribute/testing-guidelines.md b/src/docs/contribute/testing-guidelines.md index 2ff90ccbd..33b235bb7 100644 --- a/src/docs/contribute/testing-guidelines.md +++ b/src/docs/contribute/testing-guidelines.md @@ -3,9 +3,9 @@ Tools used to test the application: * **ESLint** (static code analysis of JS files) -* **Markdownlint** (static analysis of MD files) * **Stylelint** (static code analysis of CSS files) -* **Jest** (unit testing) +* **Markdownlint** (static code analysis of Markdown files) +* **Jest** (unit tests) * **Playwright** (visual and functional component testing) Generally, `npm test` and `npm run test:playwright-ct:all` should be run within @@ -13,17 +13,13 @@ their designated Docker containers before pushing changes to the repository. ## Tools -### Linters (ESlint, Markdownlint, Stylelint) - -**On host:** - -[Open][gh-gg-node-shell] `node_shell` Docker container: +You can run all tests with a single command: ```bash -docker compose run --rm node_shell +npm run lint && npm test && npm run test:playwright-ct:all ``` -**Within `node_shell`:** +### Linters (ESLint, Markdownlint, Stylelint) Run linters either all together: @@ -39,52 +35,18 @@ npm run ### Jest -**On host:** - -[Open][gh-gg-node-shell] `node_shell` Docker container: - -```bash -docker compose run --rm node_shell -``` - -**Within `node_shell`:** - -Run Jest tests: - ```bash npm run test:jest ``` ### Playwright -Playwright tests must be run in a Docker container to ensure a uniform -environment. Otherwise, snapshots would differ between operating systems. - -This is the reason why you need to run Playwright tests separately -from other tools. - #### Configuration -Test parameters can be tweaked by creating and tweaking `.env.playwright` file: - -```bash -cp .env.playwright.dist .env.playwright -``` +Test parameters can be tweaked by creating and tweaking `.env` file. #### Running Tests -Playwright tests can be run using the following commands: - -**On host:** - -[Open][gh-gg-playwright] `playwright` Docker container: - -```bash -docker compose run --rm --service-ports playwright -``` - -**Within `playwright`:** - Run tests: ```bash @@ -116,6 +78,4 @@ Then open the displayed URL (typically `http://localhost:9323`) in your browser. Please note that the test report is only available if the tests were run prior to serving the report. -[gh-gg-node-shell]: ./general-guidelines.md#node-shell -[gh-gg-playwright]: ./general-guidelines.md#playwright [playwright-cli]: https://playwright.dev/docs/test-cli#reference diff --git a/tests/playwright/env/parseDotEnvFile.ts b/tests/playwright/env/parseDotEnvFile.ts index 02aa3a346..9f1ac73aa 100644 --- a/tests/playwright/env/parseDotEnvFile.ts +++ b/tests/playwright/env/parseDotEnvFile.ts @@ -1,6 +1,6 @@ import dotenv from 'dotenv'; -const PLAYWRIGHT_ENV_FILE = '.env.playwright'; +const PLAYWRIGHT_ENV_FILE = '.env'; /** * Load and parse Playwright environment file into an object.