diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 081f025cf..c03dee0ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,37 +75,6 @@ jobs: run: | RUSTDOCFLAGS='--deny warnings' cargo doc --no-deps - frontend_tests: - name: frontend tests - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup NodeJS - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: "pnpm" - - - name: Install Rust toolchain from file - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - # Don't override flags in cargo config files. - rustflags: "" - - - name: Build frontend - run: | - pnpm install - pnpm --filter ./packages/frontend run build -- --mode development - - - name: Run frontend tests - run: | - pnpm --filter ./packages/frontend run test:no-backend - ui_components_tests: name: ui-components tests runs-on: ubuntu-latest @@ -131,37 +100,6 @@ jobs: - name: Run ui-components tests run: pnpm --filter ./packages/ui-components run test - backend_dev_setup: - name: backend dev setup - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain from file - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup wasm-pack - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: 'v0.13.1' - - - name: Run dev setup - run: | - docker run --name catcolab-postgres -e POSTGRES_USER=postgres-user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=catcolab -p 5432:5432 -d postgres:15 - cd packages/notebook-types - pnpm run build:node - cd ../automerge-doc-server - pnpm install - cd ../backend - cp .env.development .env && cp .env.development ../migrator/.env - # due to how we are detecting deploy vs dev we need to clear this env variable - unset INVOCATION_ID - cargo run -p migrator apply - npm_checks: name: npm checks runs-on: ubuntu-latest @@ -208,3 +146,26 @@ jobs: - name: Build the NixOS system for catcolab-next run: nix build .#nixosConfigurations.catcolab-next.config.system.build.toplevel + frontend_tests_with_backend: + name: frontend tests with backend + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v25 + with: + install_url: https://releases.nixos.org/nix/nix-2.29.2/install + + - name: Configure Cachix + uses: cachix/cachix-action@v14 + with: + name: catcolab-jmoggr + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Run frontend tests with VM + run: | + nix build .#checks.x86_64-linux.frontendTests -L --no-sandbox + diff --git a/dev-docs/.gitignore b/dev-docs/.gitignore deleted file mode 100644 index b2c67a2b7..000000000 --- a/dev-docs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -README.md -output/ diff --git a/dev-docs/README.md b/dev-docs/README.md new file mode 100644 index 000000000..b7fa1c3cd --- /dev/null +++ b/dev-docs/README.md @@ -0,0 +1,40 @@ +# Developer Documentation + +This directory contains documentation for CatColab developers working on testing, deployment, and local development workflows. + +## Guides + +### [Testing Guide](docs/testing-guide.md) +Learn how to run integration tests, test deployments, and debug test failures with a progressive isolation strategy. + +### [Development Workflows](docs/workflows.md) +Example workflows demonstrating how to use `cc-utils` script for common development tasks. + +## Reference + +### [cc-utils vm](docs/references/cc-utils-vm.md) +Reference for `cc-utils vm` commands used to manage local NixOS VMs. + +### [cc-utils db](docs/references/cc-utils-db.md) +Reference for `cc-utils db` commands used to manage databases across environments. + +## Quick Start + +**Preview remote branch:** +```bash +cc-utils vm start my-feature-branch --db cached +``` + +**Test a deployment:** +```bash +cc-utils vm test-deploy +``` + +**Work with databases:** +```bash +# Dump staging database +cc-utils db dump --from staging + +# Load most recent staging dump into local db +cc-utils db load --from staging +``` diff --git a/dev-docs/docs/references/cc-utils-db.md b/dev-docs/docs/references/cc-utils-db.md new file mode 100644 index 000000000..21d3aacd2 --- /dev/null +++ b/dev-docs/docs/references/cc-utils-db.md @@ -0,0 +1,52 @@ +# cc-utils DB Commands Reference + +The `cc-utils db` commands provide tools for managing databases across environments (local, staging, production, vm). + +## Available Commands + +### connect + +```bash +cc-utils db connect +``` + +Opens a `psql` connection to the specified database. + +**Targets:** `local`, `staging`, `production`, `vm` + +### dump + +```bash +cc-utils db dump [--from ] [--to ] [--quiet] +``` + +Creates a SQL dump file. Defaults to `local` and generates a timestamped filename. + +**Options:** +- `--from`: Source database (local, staging, production, vm) +- `--to`: Output filename (stored in `./dumps/`) +- `--quiet`: Only output the dump file path + +### load + +```bash +cc-utils db load --from [--to ] [--skip-migrations] +``` + +Loads a SQL dump into a database and runs migrations by default. + +**Options:** +- `--from`: Dump file path or source name (local, staging, production) for most recent dump +- `--to`: Target database (local or vm) +- `--skip-migrations`: Skip running migrations after loading + +### reset + +```bash +cc-utils db reset [--skip-migrations] +``` + +Drops and recreates the local database. Only supports `local` target. + +**Options:** +- `--skip-migrations`: Skip running migrations after reset diff --git a/dev-docs/docs/references/cc-utils-vm.md b/dev-docs/docs/references/cc-utils-vm.md new file mode 100644 index 000000000..dff3efae6 --- /dev/null +++ b/dev-docs/docs/references/cc-utils-vm.md @@ -0,0 +1,82 @@ +# cc-utils VM Commands Reference + +The `cc-utils vm` commands provide tools for managing local NixOS VMs for testing and development. + +## Available Commands + +### start + +```bash +cc-utils vm start [branch] [--db skip|cached|] +``` + +Starts a VM, optionally from a specific GitHub branch. + +**Arguments:** +- `branch` (optional) - GitHub branch name to build VM from. If omitted, uses local nix configuration. + +**Database options:** +- No `--db` flag (default) - Load fresh staging database dump +- `--db skip` - Don't load any database +- `--db cached` - Use most recent staging dump in dumps/, or create new if missing +- `--db ` - Load from specific dump file path + +**Examples:** +```bash +# Start local VM with fresh staging DB +cc-utils vm start + +# Start VM from branch with cached DB +cc-utils vm start my-feature-branch --db cached + +# Start without loading database +cc-utils vm start --db skip + +# Start with specific dump file +cc-utils vm start --db ./dumps/my_dump.sql +``` + +### stop + +```bash +cc-utils vm stop +``` + +Gracefully shuts down the running VM. + +### status + +```bash +cc-utils vm status +``` + +Shows whether the VM is running and displays the log file location. + +### logs + +```bash +cc-utils vm logs [--follow] +``` + +Displays VM logs. Use `--follow` (or `-f`) to tail the logs in real-time. + +### connect + +```bash +cc-utils vm connect +``` + +Opens an SSH connection to the running VM. Useful for inspecting the VM state or running commands manually. + +### test-deploy + +```bash +cc-utils vm test-deploy +``` + +Comprehensive test workflow that: +1. Builds a VM from the current staging branch on GitHub +2. Starts the VM +3. Copies the staging database to the VM +4. Deploys your current local branch to the running VM +5. Runs the frontend tests diff --git a/dev-docs/docs/testing-guide.md b/dev-docs/docs/testing-guide.md new file mode 100644 index 000000000..db0480ab1 --- /dev/null +++ b/dev-docs/docs/testing-guide.md @@ -0,0 +1,133 @@ +# Testing Guide + +## Running Integration Tests + +To run the full integration tests (same as CI): + +```bash +nix build .#checks.x86_64-linux.frontendTests -L --no-sandbox +``` + +This command runs the complete test suite in a NixOS VM, exactly as it runs in CI. + +## Test Deployment + +To test a deployment locally: + +```bash +cc-utils vm test-deploy +``` + +This command: +1. Builds a VM from the current staging branch on GitHub +2. Starts the VM +3. Copies the staging database to the VM +4. Deploys your current local branch to the running VM +5. Runs the frontend tests + +## Testing Individual Nix Packages +```bash +# Build the nix package for backend +nix build .#backend + +# Run the nix-built backend +# This can be used alongside the development services for frontend and automerge-doc-server to complete +# the app +./result/bin/backend +``` + +## Debugging Test Failures + +When integration tests or deployment tests fail, debugging directly through Nix commands is slow due to long cycle times. The issue is usually in your code, not the Nix configuration. + +This progressive strategy isolates the problem by testing in increasingly complex environments: + +### Step 1: Sanity Check - Local Development Environment + +**Frontend:** +```bash +cd packages/frontend +npm run test +``` + +**Backend:** +Start the automerge-doc-server and backend as you normally would for development. + +**What this tests:** Basic functionality in your standard development setup. + +### Step 2: CI Configuration Check + +**Frontend:** +```bash +cd packages/frontend +npm run test:ci +``` + +**Backend:** +Same as Step 1 (development servers running). + +**What this tests:** Whether the CI-specific vitest configuration causes test failures. + +### Step 3: Nix Package Verification + +**Frontend:** +```bash +nix build .#frontend-tests +./result/bin/frontend-tests +``` + +**Backend:** +Same as Step 1 (development servers running). + +**What this tests:** Whether the Nix packaging of the frontend tests is working properly. + +### Step 4: VM Backend Isolation + +**Frontend:** +```bash +cd packages/frontend +npm run test +``` + +**Backend:** +Stop your development servers and start the VM: +```bash +cc-utils vm start +``` + +**What this tests:** Backend packaging, deployment, and VM configuration issues. + +### Step 5: Full Integration + +**Frontend:** +Same as Step 3 (Nix-built frontend-tests). + +**Backend:** +Same as Step 4 (VM running via `cc-utils vm start`). + +**What this tests:** The complete integration as it runs in CI, fully isolated from your development environment. + +### Step 6: Manual Test Execution in VM + +**Frontend:** +SSH into the running VM and execute tests manually: +```bash +cc-utils vm connect +frontend-tests +``` + +**Backend:** +VM must be running (use `cc-utils vm start` if needed). + +**What this tests:** Manual execution inside the VM environment, useful for debugging test runner issues or inspecting the VM state. + +### Beyond Step 6: Debugging Nix Test Environment + +If you are unable to reproduce the problem after Step 6, then there is likely a problem somewhere the nix +configuration. This would be a good time to bug Jason, or failing that, to cancel your weekend plans (/s, +but nix commands are slow, causing painfully long cycle times). + +This will start an interactive Python shell in the Nix test environment: +```bash +nix build .#checks.x86_64-linux.frontendTests.driverInteractive +``` diff --git a/dev-docs/docs/workflows.md b/dev-docs/docs/workflows.md new file mode 100644 index 000000000..815786982 --- /dev/null +++ b/dev-docs/docs/workflows.md @@ -0,0 +1,47 @@ +# Development Workflows + +## Review a branch locally + +```bash +# Start a VM from a remote branch with the staging database +# Uses the most recent staging dump in ./dumps/, or creates one if missing +cc-utils vm start my-feature-branch --db cached + +# App available at http://localhost:8000 +# No need to stop local development services + +# Stop the VM when done +cc-utils vm stop +``` + +## Testing a migration against staging data + +```bash +# Dump the staging database +# This will print the dump file path, e.g.: ./dumps/staging_dump_20240115_143022.sql +cc-utils db dump --from staging + +# Create your migration in the migrator package +# See packages/migrator/README.md for instructions on creating migrations + +# Load the dump into your local db (migrations run automatically after loading) +cc-utils db load --from ./dumps/staging_dump_20240115_143022.sql + +# To iterate, edit the migration and re-run the load command +``` + +## Flexible development with db snapshots + +```bash +# Dump the local database to ./dumps/local_snapshot.sql +cc-utils db dump --to local_snapshot.sql + +# Load the snapshot without running migrations +cc-utils db load --from ./dumps/local_snapshot.sql --skip-migrations + +# Manually run the migrations +cargo run -p migrator apply + +# Get a clean DB +cc-utils db reset +``` diff --git a/flake.nix b/flake.nix index 754844b63..9dbe2e7ae 100644 --- a/flake.nix +++ b/flake.nix @@ -185,9 +185,6 @@ }) devShellSystems ); - # Example of how to build and test individual package built by nix: - # nix build .#packages.x86_64-linux.automerge - # node ./result/main.cjs packages = { x86_64-linux = { catcolabApi = pkgsLinux.stdenv.mkDerivation { @@ -226,22 +223,22 @@ inherit inputs rustToolchainLinux self; }; - frontend = pkgsLinux.callPackage ./packages/frontend/default.nix { - inherit inputs rustToolchainLinux self; - }; + frontend = + (pkgsLinux.callPackage ./packages/frontend/default.nix { + inherit inputs rustToolchainLinux self; + }).package; + + frontend-tests = + (pkgsLinux.callPackage ./packages/frontend/default.nix { + inherit inputs rustToolchainLinux self; + }).tests; # VMs built with `nixos-rebuild build-vm` (like `nix build # .#nixosConfigurations.catcolab-vm.config.system.build.vm`) are not the same # as "traditional" VMs, which causes deploy-rs to fail when deploying to them. # https://github.com/serokell/deploy-rs/issues/85#issuecomment-885782350 # - # This is worked around by creating a full featured VM image. - # - # use: - # nix build .#catcolab-vm - # cp result/catcolab-vm.qcow2 catcolab-vm.qcow2 - # db-utils vm start - # deploy -s .#catcolab-vm + # This is worked around by creating a full featured QEMU VM image. catcolab-vm = pkgsLinux.stdenv.mkDerivation { name = "catcolab-vm"; src = nixos-generators.nixosGenerate { @@ -335,16 +332,14 @@ }; }; - # Temporarily disabled until more meaningful tests are developed. Keeping the frontend dependecies - # up to date is currently not worth the hassle. - # checks.x86_64-linux.integrationTests = import ./infrastructure/tests/integration.nix { - # inherit - # nixpkgs - # inputs - # self - # linuxSystem - # ; - # rustToolchain = rustToolchainLinux; - # }; + checks.x86_64-linux.frontendTests = import ./infrastructure/tests/frontend.nix { + inherit + nixpkgs + inputs + self + linuxSystem + ; + rustToolchain = rustToolchainLinux; + }; }; } diff --git a/infrastructure/hosts/catcolab-next/default.nix b/infrastructure/hosts/catcolab-next/default.nix index 905453010..e30c96a19 100644 --- a/infrastructure/hosts/catcolab-next/default.nix +++ b/infrastructure/hosts/catcolab-next/default.nix @@ -8,6 +8,7 @@ let owen = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF2sBTuqGoEXRWpBRqTBwZZPDdLGGJ0GQcuX5dfIZKb4 o@red-special"; epatters = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAKXx6wMJSeYKCHNmbyR803RQ72uto9uYsHhAPPWNl2D evan@epatters.org"; jmoggr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMiaHaeJ5PQL0mka/lY1yGXIs/bDK85uY1O3mLySnwHd j@jmoggr.com"; + kasbah = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1K/FB6dCjo1/xfddi9VoHEGchFo/bcz6v7SC7wAuFQ kaspar@topos"; catcolab-next-deployuser = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM7AYg1fZM0zMxb/BuZTSwK4O3ycUIHruApr1tKoO8nJ deployuser@next.catcolab.org"; in { @@ -49,6 +50,7 @@ in epatters jmoggr catcolab-next-deployuser + kasbah ]; backup = { enable = true; diff --git a/infrastructure/hosts/catcolab-vm/default.nix b/infrastructure/hosts/catcolab-vm/default.nix index f88af61d7..723b5c7ed 100644 --- a/infrastructure/hosts/catcolab-vm/default.nix +++ b/infrastructure/hosts/catcolab-vm/default.nix @@ -19,20 +19,22 @@ catcolab = { enable = true; + enableCaddy = false; backend = { port = 8000; - hostname = "backend-next.catcolab.org"; + hostname = ""; serveFrontend = true; }; automerge = { port = 8010; - hostname = "automerge-next.catcolab.org"; + hostname = ""; }; environmentFile = /etc/catcolab/catcolab-secrets.env; host = { enable = true; userKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMiaHaeJ5PQL0mka/lY1yGXIs/bDK85uY1O3mLySnwHd j@jmoggr.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1K/FB6dCjo1/xfddi9VoHEGchFo/bcz6v7SC7wAuFQ kaspar@topos" ]; }; }; @@ -51,8 +53,34 @@ 5432 ]; + virtualisation.vmVariant = { + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 8000; + guest.port = 8000; + } + { + from = "host"; + host.port = 8010; + guest.port = 8010; + } + { + from = "host"; + host.port = 2221; + guest.port = 22; + } + { + from = "host"; + host.port = 5433; + guest.port = 5432; + } + ]; + }; + # This matches the default root device that is created by nixos-generators fileSystems."/".device = "/dev/disk/by-label/nixos"; + virtualisation.diskSize = 20 * 1024; services.qemuGuest.enable = true; # needed for deploy-rs to works @@ -60,6 +88,7 @@ enable = true; device = "/dev/vda"; }; + services.getty.autologinUser = "catcolab"; networking.hostName = "catcolab-vm"; diff --git a/infrastructure/modules/catcolab/services.nix b/infrastructure/modules/catcolab/services.nix index 93851570d..f8fa6384e 100644 --- a/infrastructure/modules/catcolab/services.nix +++ b/infrastructure/modules/catcolab/services.nix @@ -9,6 +9,7 @@ let cfg = config.catcolab; frontendPkg = self.packages.${pkgs.system}.frontend; + frontendTestsPkg = self.packages.${pkgs.system}.frontend-tests; backendPkg = self.packages.${pkgs.system}.backend; automergePkg = self.packages.${pkgs.system}.automerge; @@ -43,6 +44,12 @@ with lib; options.catcolab = { enable = lib.mkEnableOption "Catcolab services"; + enableCaddy = mkOption { + type = types.bool; + default = true; + description = "Enable Caddy reverse proxy for the backend and automerge services."; + }; + backend = { port = mkOption { type = types.port; @@ -90,7 +97,7 @@ with lib; groups.catcolab = { }; }; - networking.firewall.allowedTCPPorts = [ + networking.firewall.allowedTCPPorts = lib.mkIf cfg.enableCaddy [ 80 443 ]; @@ -98,6 +105,7 @@ with lib; environment.systemPackages = [ backendPkg automergePkg + frontendTestsPkg databaseSetupScript ]; @@ -135,7 +143,7 @@ with lib; serviceConfig = { User = "catcolab"; - Type = "notify"; + Type = "simple"; Restart = "on-failure"; ExecStart = lib.getExe backendPkg; EnvironmentFile = cfg.environmentFile; @@ -146,6 +154,13 @@ with lib; enable = true; wantedBy = [ "multi-user.target" ]; + # Bind automerge lifecycle to backend + bindsTo = [ "backend.service" ]; + partOf = [ "backend.service" ]; + + # Only need to wait for backend (which already waits for DB and network) + after = [ "backend.service" ]; + environment = { PORT = automergePortStr; }; @@ -159,7 +174,7 @@ with lib; }; }; - services.caddy = { + services.caddy = lib.mkIf cfg.enableCaddy { enable = true; virtualHosts = { "${cfg.backend.hostname}" = { diff --git a/infrastructure/scripts/cc-utils b/infrastructure/scripts/cc-utils new file mode 100755 index 000000000..477134652 --- /dev/null +++ b/infrastructure/scripts/cc-utils @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Source library files +source "$(dirname "${BASH_SOURCE[0]}")/cc-utils-lib.sh" +source "$(dirname "${BASH_SOURCE[0]}")/cc-utils-db.sh" +source "$(dirname "${BASH_SOURCE[0]}")/cc-utils-vm.sh" + +# Global variables for for tracking ssh tunnels, see open_tunnel +STAGING_SSH_PID="" +PROD_SSH_PID="" + +# The local ports to use when tunnelling remote PostgreSQL ports +STAGING_LOCAL_PGPORT="5434" +PROD_LOCAL_PGPORT="5435" + +function print_help { + echo "Usage: $0 [options]" + echo "" + echo "Subcommands:" + echo " db Database management commands" + echo " vm Virtual machine commands" + echo "" + echo "Use '$0 --help' for more information." +} + +# Entry point +subcommand="$1" +shift || true + +case "$subcommand" in + db) + run_db "$@" + ;; + vm) + run_vm "$@" + ;; + "-h" | "--help" | "") + print_help + ;; + *) + echo "Unknown subcommand: $subcommand" + echo "" + print_help + exit 1 + ;; +esac diff --git a/infrastructure/scripts/db-utils b/infrastructure/scripts/cc-utils-db.sh old mode 100755 new mode 100644 similarity index 77% rename from infrastructure/scripts/db-utils rename to infrastructure/scripts/cc-utils-db.sh index 12240733a..1d3785538 --- a/infrastructure/scripts/db-utils +++ b/infrastructure/scripts/cc-utils-db.sh @@ -2,16 +2,10 @@ set -euo pipefail -source "$(dirname "${BASH_SOURCE[0]}")/db-utils-lib.sh" +# Database subcommand functions -STAGING_SSH_PID="" -STAGING_LOCAL_PGPORT="5434" - -PROD_SSH_PID="" -PROD_PGPORT="5433" - -function print_help { - echo "Usage: $0 [options]" +function print_db_help { + echo "Usage: $0 db [options]" echo "" echo "Subcommands:" echo " reset Reset the local database" @@ -19,25 +13,26 @@ function print_help { echo " dump Dump a target (local|staging|production) database" echo " connect Connect to a target (local|staging|production)" echo "" - echo "Use '$0 --help' for more information." + echo "Use '$0 db --help' for more information." } function print_connect_help { - echo "Usage: $0 connect " + echo "Usage: $0 db connect " echo "Targets: local, staging, production" } function print_dump_help { - echo "Usage: $0 dump [options]" + echo "Usage: $0 db dump [options]" echo "" echo "Options:" echo " -f, --from Target: local (default), staging, or production" echo " -t, --to Filename to write (defaults to './dumps/_dump_.sql')" + echo " -q, --quiet Only output the dump file path" echo " -h, --help Print this help message" } function print_load_help { - echo "Usage: $0 load [options]" + echo "Usage: $0 db load [options]" echo "" echo "Options:" echo " -t, --to Target: local (default), staging, or production" @@ -50,7 +45,7 @@ function print_load_help { } function print_reset_help { - echo "Usage: $0 reset [options]" + echo "Usage: $0 db reset [options]" echo "" echo "Reset the local database by dropping and recreating it." echo "" @@ -60,6 +55,35 @@ function print_reset_help { echo " -h, --help Print this help message" } +function run_db() { + local subcommand="${1-}" + shift || true + + case "$subcommand" in + connect) + run_connect "$@" + ;; + dump) + run_dump "$@" + ;; + load) + run_load "$@" + ;; + reset) + run_reset "$@" + ;; + "-h" | "--help" | "") + print_db_help + ;; + *) + echo "Unknown db subcommand: $subcommand" + echo "" + print_db_help + exit 1 + ;; + esac +} + function run_connect() { local target="$1" if [[ "$target" == "-h" || "$target" == "--help" ]]; then @@ -84,6 +108,7 @@ function run_connect() { function run_dump() { local target="local" local dump_file="" + local quiet=false while [[ ${1-} ]]; do case "$1" in @@ -95,6 +120,9 @@ function run_dump() { shift dump_file="$1" ;; + "-q" | "--quiet") + quiet=true + ;; "-h" | "--help") print_dump_help exit 0 @@ -120,9 +148,14 @@ function run_dump() { case "$target" in local|vm|staging|production) - echo "Dumping $target to $dump_file..." + if [[ "$quiet" != "true" ]]; then + echo "Dumping $target to $dump_file..." + fi load_target_env "$target" pg_dump --clean --if-exists > "$dump_file" + if [[ "$quiet" == "true" ]]; then + echo "$dump_file" + fi ;; *) echo "Unknown target: $target" @@ -166,7 +199,7 @@ function run_load() { done if [[ -z $dump_file ]]; then - echo "Missing dump specifier." + echo "Error: Missing dump specifier" print_load_help exit 1 fi @@ -192,9 +225,13 @@ function run_load() { case "$target" in local|vm) + if ! confirm_action "WARNING: This will overwrite the $target database."; then + exit 1 + fi + load_target_env "$target" echo "Loading into $target..." - psql --dbname="$PGDATABASE" -f "$dump_file" + psql -q --dbname="$PGDATABASE" -f "$dump_file" if [[ "$skip_migrate" != "true" ]]; then echo "" @@ -202,8 +239,7 @@ function run_load() { fi ;; staging|production) - echo "Not allow to modify target: $target" - echo "" + echo "Error: Not allowed to modify target: $target" exit 1 ;; *) @@ -249,10 +285,7 @@ function run_reset() { exit 1 fi - echo "WARNING: This will reset your local 'catcolab' database." - read -n1 -r -p "Continue? [y/N] " yn - if [[ $yn != [yY] ]]; then - echo "Aborted." + if ! confirm_action "WARNING: This will reset your local 'catcolab' database."; then exit 1 fi @@ -275,62 +308,3 @@ function run_reset() { run_local_migrations fi } - -function run_vm() { - local target="local" - local skip_migrate=false - - while [[ ${1-} ]]; do - case "$1" in - "start") - run_vm_start - ;; - *) - echo "Unknown argument: $1" - echo "" - print_load_help - exit 1 - ;; - esac - shift - done -} - -function run_vm_start() { - qemu-system-x86_64 \ - -enable-kvm -cpu host -smp 2 -m 2048 \ - -drive file=./catcolab-vm.qcow2,if=virtio,format=qcow2 \ - -device virtio-net-pci,netdev=net0 \ - -netdev user,id=net0,hostfwd=tcp::2221-:22,hostfwd=tcp::5433-:5432,hostfwd=tcp::8000-:8000,hostfwd=tcp::8010-:8010 -} - -# Entry point -subcommand="$1" -shift || true - -case "$subcommand" in - vm) - run_vm "$@" - ;; - connect) - run_connect "$@" - ;; - dump) - run_dump "$@" - ;; - load) - run_load "$@" - ;; - reset) - run_reset "$@" - ;; - "-h" | "--help" | "") - print_help - ;; - *) - echo "Unknown subcommand: $subcommand" - echo "" - print_help - exit 1 - ;; -esac diff --git a/infrastructure/scripts/db-utils-lib.sh b/infrastructure/scripts/cc-utils-lib.sh similarity index 58% rename from infrastructure/scripts/db-utils-lib.sh rename to infrastructure/scripts/cc-utils-lib.sh index 6b9fc79f3..bdbb7dc18 100644 --- a/infrastructure/scripts/db-utils-lib.sh +++ b/infrastructure/scripts/cc-utils-lib.sh @@ -2,6 +2,18 @@ set -euo pipefail +function confirm_action() { + local message="$1" + echo "$message" + read -n1 -r -p "Continue? [y/N] " yn + echo # newline after prompt + if [[ $yn != [yY] ]]; then + echo "Aborted." + return 1 + fi + return 0 +} + function load_env() { local varname="$1" local env_file="$2" @@ -16,7 +28,7 @@ function load_env() { exit 1 fi - # Decrypt if it’s an agenix file + # Decrypt if it's an agenix file local content if [[ $env_file == *.age ]]; then pushd "$(dirname "$env_file")" >/dev/null @@ -27,8 +39,7 @@ function load_env() { fi # extract VAR= - local url - url=$(printf '%s\n' "$content" | grep -E "^${varname}=" | cut -d '=' -f2-) + local url=$(printf '%s\n' "$content" | grep -E "^${varname}=" | cut -d '=' -f2-) if [[ -z $url ]]; then echo "Error: '$varname' missing in $env_file." >&2 exit 1 @@ -88,14 +99,14 @@ function load_target_env() { export PGPORT="5433" ;; staging) - load_env DATABASE_URL "$(find_git_root)/infrastructure/secrets/.env.next.age" + load_env DATABASE_URL "$(find_git_root)/infrastructure/secrets/env.next.age" open_tunnel "catcolab" "backend-next.catcolab.org" "$STAGING_LOCAL_PGPORT" "$PGPORT" "STAGING_SSH_PID" export PGPORT="$STAGING_LOCAL_PGPORT" ;; production) - load_env DATABASE_URL "$(find_git_root)/infrastructure/secrets/.env.prod.age" + load_env DATABASE_URL "$(find_git_root)/infrastructure/secrets/env.prod.age" open_tunnel "catcolab" "backend.catcolab.org" "$PROD_LOCAL_PGPORT" "$PGPORT" "PROD_SSH_PID" @@ -125,3 +136,88 @@ function run_local_migrations() { echo "Running local migrations..." cargo run -p migrator apply } + +# VM management functions +function get_vm_cache_dir() { + local cache_dir + if [[ -n "${XDG_CACHE_HOME:-}" ]]; then + cache_dir="$XDG_CACHE_HOME/catcolab" + elif [[ "$(uname)" == "Darwin" ]]; then + cache_dir="$HOME/Library/Caches/catcolab" + else + cache_dir="$HOME/.cache/catcolab" + fi + mkdir -p "$cache_dir" + echo "$cache_dir" +} + +function get_vm_log_file() { + echo "$(get_vm_cache_dir)/vm.log" +} + +function get_vm_monitor_socket() { + echo "$(get_vm_cache_dir)/vm-monitor.sock" +} + +function vm_is_running() { + local monitor_socket="$(get_vm_monitor_socket)" + + # Check if socket exists and is connectable + if [[ -S "$monitor_socket" ]]; then + # Try to connect to verify it's active (without sending a command) + if echo | socat -T 1 - "UNIX-CONNECT:$monitor_socket" >/dev/null 2>&1; then + return 0 + fi + fi + + # Stale socket file + rm -f "$monitor_socket" + return 1 +} + +function wait_for_vm_shutdown() { + local timeout="${1:-30}" + local elapsed=0 + + while vm_is_running; do + if (( elapsed >= timeout )); then + return 1 + fi + sleep 1 + (( elapsed++ )) + done + + return 0 +} + +function wait_for_backend() { + local timeout="${1:-60}" + local backend_url="http://localhost:8000/status" + local elapsed=0 + + while [ $elapsed -lt $timeout ]; do + if curl -s "$backend_url" >/dev/null 2>&1; then + return 0 + fi + + if [ $elapsed -ge $timeout ]; then + return 1 + fi + + sleep 1 + elapsed=$((elapsed + 1)) + done + + return 1 +} + +function print_vm_ready() { + local message="${1:-VM is ready!}" + echo "" + echo "$message" + echo " Backend: http://localhost:8000" + echo " Database: localhost:5433" + echo " SSH: cc-utils vm connect" + echo "" + echo "To stop: cc-utils vm stop" +} diff --git a/infrastructure/scripts/cc-utils-vm.sh b/infrastructure/scripts/cc-utils-vm.sh new file mode 100644 index 000000000..1fc3c8b8e --- /dev/null +++ b/infrastructure/scripts/cc-utils-vm.sh @@ -0,0 +1,403 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# SSH options for connecting to the VM +# These options skip host key verification for the local development VM +VM_SSH_OPTS="-p 2221 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + +# VM subcommand functions + +function print_vm_help { + echo "Usage: $0 vm [options]" + echo "" + echo "Subcommands:" + echo " start [--skip-db] Start the VM in background (with staging data by default)" + echo " stop Stop the running VM" + echo " status Show VM status" + echo " connect Connect to the VM via SSH" + echo " test-deploy Build VM from staging, load data, test deploy" + echo " logs [--follow] Show VM logs (use -f or --follow to tail)" + echo "" + echo "Use '$0 vm --help' for more information." +} + +function run_vm() { + local subcommand="${1-}" + shift || true + + case "$subcommand" in + start) + run_vm_start "$@" + ;; + stop) + run_vm_stop "$@" + ;; + status) + run_vm_status "$@" + ;; + logs) + run_vm_logs "$@" + ;; + connect) + run_vm_connect "$@" + ;; + test-deploy) + run_vm_test_deploy "$@" + ;; + "-h" | "--help" | "") + print_vm_help + ;; + *) + echo "Unknown vm subcommand: $subcommand" + echo "" + print_vm_help + exit 1 + ;; + esac +} + + +function run_vm_stop() { + if ! vm_is_running; then + echo "VM is not running" + exit 1 + fi + + local monitor_socket="$(get_vm_monitor_socket)" + + echo "Stopping VM..." + # Send graceful shutdown via QEMU monitor + if [[ -S "$monitor_socket" ]]; then + echo "Sending ACPI power button event..." + echo "system_powerdown" | socat - "UNIX-CONNECT:$monitor_socket" 2>/dev/null || true + + if wait_for_vm_shutdown 30; then + echo "VM stopped" + rm -f "$monitor_socket" + return 0 + fi + + echo "Error: VM did not shut down within timeout" + echo "Check logs: $(get_vm_log_file)" + exit 1 + else + echo "Error: Monitor socket not found" + exit 1 + fi +} + +function run_vm_status() { + if vm_is_running; then + echo "VM is running" + echo "Logs: $(get_vm_log_file)" + else + echo "VM is not running" + exit 1 + fi +} + +function run_vm_logs() { + local log_file="$(get_vm_log_file)" + local follow=false + + while [[ ${1-} ]]; do + case "$1" in + "-f"|"--follow") + follow=true + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac + shift + done + + if [[ ! -f "$log_file" ]]; then + echo "No log file found at $log_file" + exit 1 + fi + + if [[ "$follow" == "true" ]]; then + tail -f "$log_file" + else + # Show ANSI colors and prevent screen clearing + less -RX "$log_file" + fi +} + +function run_vm_connect() { + if ! vm_is_running; then + echo "VM is not running. Start it first with: $0 vm start" + exit 1 + fi + + echo "Connecting to VM via SSH..." + + if ! ssh $VM_SSH_OPTS catcolab@localhost; then + echo "" + echo "Note: If authentication failed, your SSH public key must be added to" + echo " 'env.next.age' in infrastructure/secrets/secrets.nix" + exit 1 + fi +} + +function get_or_create_staging_dump() { + # Find most recent staging dump in dumps/ + local dumps_dir="$(find_git_root)/dumps" + mkdir -p "$dumps_dir" + + local recent_dump=$(ls -t "$dumps_dir"/staging_dump_*.sql 2>/dev/null | head -1) + + if [[ -n "$recent_dump" && -f "$recent_dump" ]]; then + echo "$recent_dump" + return 0 + fi + + # No cached dump found, create a new one + echo "No cached staging dump found, creating new dump..." >&2 + local dump_file=$(run_dump --from staging --quiet) + echo "$dump_file" +} + +function load_dump_to_vm() { + local dump_file="$1" + + if [[ ! -f "$dump_file" ]]; then + echo "Error: Dump file not found: $dump_file" + return 1 + fi + + echo "Stopping backend service..." + ssh $VM_SSH_OPTS catcolab@localhost "sudo systemctl stop backend" + + echo "Loading database into VM..." + if ! run_load --to vm --from "$dump_file" --skip-migrations; then + echo "Error: Failed to load database" + return 1 + fi + + echo "Starting backend service (migrations will run on loaded data)..." + ssh $VM_SSH_OPTS catcolab@localhost "sudo systemctl start backend" + if ! wait_for_backend 60; then + echo "Error: Timeout waiting for backend service to restart" + return 1 + fi + + echo "Backend ready" + return 0 +} + +function load_staging_db_to_vm() { + echo "Dumping staging database..." + local dump_file=$(run_dump --from staging --quiet) + if [[ ! -f "$dump_file" ]]; then + echo "Error: Failed to create staging dump" + return 1 + fi + + load_dump_to_vm "$dump_file" +} + +function run_vm_start() { + local branch="" + local db_source="default" + + # Parse positional branch argument first + if [[ ${1-} && ! "$1" =~ ^-- ]]; then + branch="$1" + shift + fi + + # Parse options + while [[ ${1-} ]]; do + case "$1" in + "--db") + shift + db_source="$1" + ;; + "-h"|"--help") + echo "Usage: $0 vm start [branch] [--db skip|cached|]" + echo "" + echo "Arguments:" + echo " branch Optional: GitHub branch to build VM from" + echo "" + echo "Options:" + echo " --db skip Don't load any database" + echo " --db cached Use most recent staging dump or create new" + echo " --db Load from specific dump file" + echo " (default) Fresh staging dump (no --db flag)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown argument: $1" + echo "Use '$0 vm start --help' for more information." + exit 1 + ;; + esac + shift + done + + if vm_is_running; then + echo "VM is already running" + exit 1 + fi + + local monitor_socket="$(get_vm_monitor_socket)" + local log_file="$(get_vm_log_file)" + + # Build and start VM based on whether branch is specified + if [[ -n "$branch" ]]; then + # Build VM from GitHub branch + echo "Building VM from branch: $branch..." + if ! nix build "github:ToposInstitute/CatColab/$branch#catcolab-vm"; then + echo "Error: Failed to build VM from branch: $branch" + exit 1 + fi + + echo "Copying VM disk image to working directory..." + local vm_disk="./catcolab-vm.qcow2" + if [[ ! -f "result/catcolab-vm.qcow2" ]]; then + echo "Error: Built VM image not found at result/catcolab-vm.qcow2" + exit 1 + fi + + # Nix store files are read-only, make writable for QEMU + cp --remove-destination "result/catcolab-vm.qcow2" "$vm_disk" + chmod +w "$vm_disk" + + echo "Starting VM with QEMU..." + # Start QEMU VM in background + qemu-system-x86_64 \ + -enable-kvm -cpu host -smp 2 -m 2048 \ + -nographic \ + -drive file="$vm_disk",if=virtio,format=qcow2 \ + -device virtio-net-pci,netdev=net0 \ + -netdev user,id=net0,hostfwd=tcp::2221-:22,hostfwd=tcp::5433-:5432,hostfwd=tcp::8000-:8000,hostfwd=tcp::8010-:8010 \ + -monitor "unix:$monitor_socket,server,nowait" \ + >> "$log_file" 2>&1 & + else + # Start local nix VM + echo "Starting local VM..." + nix run .#nixosConfigurations.catcolab-vm.config.system.build.vm -- \ + -nographic \ + -monitor "unix:$monitor_socket,server,nowait" \ + >> "$log_file" 2>&1 & + fi + + echo "Logs: cc-utils vm logs" + + echo "Waiting for VM to start..." + if ! wait_for_backend 60; then + echo "Error: Timeout waiting for VM to be available" + echo "Check logs: $log_file" + exit 1 + fi + + # Handle database loading based on --db option + case "$db_source" in + "skip") + echo "Skipping database load" + ;; + "cached") + echo "Using cached staging dump..." + local dump_file=$(get_or_create_staging_dump) + if ! load_dump_to_vm "$dump_file"; then + echo "Error: Failed to load cached dump" + exit 1 + fi + ;; + "default") + echo "Loading fresh staging database..." + if ! load_staging_db_to_vm; then + echo "Error: Failed to load staging database" + exit 1 + fi + ;; + *) + # Treat as file path + echo "Loading database from: $db_source" + if ! load_dump_to_vm "$db_source"; then + echo "Error: Failed to load database from file" + exit 1 + fi + ;; + esac + + print_vm_ready +} + +function run_vm_test_deploy() { + if vm_is_running; then + echo "Error: VM is already running" + echo "Please stop it first with: cc-utils vm stop" + exit 1 + fi + + if ! confirm_action "WARNING: This will delete and recreate the VM image."; then + exit 1 + fi + + echo "Building VM from frontend-tests-ci branch..." + if ! nix build github:ToposInstitute/CatColab/frontend-tests-ci#catcolab-vm; then + echo "Error: Failed to build VM from frontend-tests-ci branch" + exit 1 + fi + + echo "Copying VM image to working directory..." + local vm_disk="./catcolab-vm.qcow2" + if [[ ! -f "result/catcolab-vm.qcow2" ]]; then + echo "Error: Built VM image not found at result/catcolab-vm.qcow2" + exit 1 + fi + + # Nix store files are read only and qemu needs to write to the vm disk image + cp --remove-destination "result/$vm_disk" "$vm_disk" + chmod +w "$vm_disk" + + echo "Starting VM with QEMU..." + local monitor_socket="$(get_vm_monitor_socket)" + local log_file="$(get_vm_log_file)" + + # Start QEMU vm in background + qemu-system-x86_64 \ + -enable-kvm -cpu host -smp 2 -m 2048 \ + -nographic \ + -drive file="$vm_disk",if=virtio,format=qcow2 \ + -device virtio-net-pci,netdev=net0 \ + -netdev user,id=net0,hostfwd=tcp::2221-:22,hostfwd=tcp::5433-:5432,hostfwd=tcp::8000-:8000,hostfwd=tcp::8010-:8010 \ + -monitor "unix:$monitor_socket,server,nowait" \ + >> "$log_file" 2>&1 & + + echo "Logs: cc-utils vm logs" + + echo "Waiting for VM to start..." + if ! wait_for_backend 60; then + echo "Error: Timeout waiting for VM to be available" + echo "Check logs: $log_file" + exit 1 + fi + + echo "Loading staging database..." + if ! load_staging_db_to_vm; then + echo "Error: Failed to load staging database" + exit 1 + fi + + echo "Deploying local changes to VM..." + if ! deploy -s --ssh-opts="$VM_SSH_OPTS" .#catcolab-vm; then + echo "Error: Deployment failed" + exit 1 + fi + + echo "Running frontend tests..." + if ! ssh $VM_SSH_OPTS catcolab@localhost "frontend-tests"; then + echo "Error: Frontend tests failed" + echo "Check VM logs: cc-utils vm logs" + exit 1 + fi + + print_vm_ready "Test deployment complete!" +} diff --git a/infrastructure/secrets/env.next.age b/infrastructure/secrets/env.next.age index 91465f95c..b0f08d0c0 100644 Binary files a/infrastructure/secrets/env.next.age and b/infrastructure/secrets/env.next.age differ diff --git a/infrastructure/secrets/env.prod.age b/infrastructure/secrets/env.prod.age index 166c9480c..990857add 100644 Binary files a/infrastructure/secrets/env.prod.age and b/infrastructure/secrets/env.prod.age differ diff --git a/infrastructure/secrets/rclone.conf.next.age b/infrastructure/secrets/rclone.conf.next.age index 51a629cba..2eca3ad85 100644 Binary files a/infrastructure/secrets/rclone.conf.next.age and b/infrastructure/secrets/rclone.conf.next.age differ diff --git a/infrastructure/secrets/rclone.conf.prod.age b/infrastructure/secrets/rclone.conf.prod.age index 01273e07c..9c23f960c 100644 --- a/infrastructure/secrets/rclone.conf.prod.age +++ b/infrastructure/secrets/rclone.conf.prod.age @@ -1,11 +1,13 @@ age-encryption.org/v1 --> ssh-ed25519 2purlw s+KqvH58D1vzHYv+SSWsAzQBiMRgtWgsa41xYDbVV0M -RC6rZX/pw3z8d0GvnEZbPcnLATkfm70wMV1NyFI80uI --> ssh-ed25519 +EkgOg LeN98OU7Mmlun4rbWuBJ4CaCq/2pytolpHDZtiLeeXI -lGOFLyHzJtpPrv8/X7WVjpcV7ajz2AjpVDJPtgUDiPY --> ssh-ed25519 d3XP2A 0Vf76l+t0pTNi0fvoq1I1Ne9b+QYpAMH/rsnNY7/QhE -J4YKReBXbJENhDzbrOPA1dVC8BWZUKfIDf8qHhEoeWc --> ssh-ed25519 3iWz6A ZU/i1+WhcOa/EJ6CSjvvM/2CpnUqnl/punZu3seF2RY -grpGHhIf5ZeLJXJG/Lit6CuGLSDz6m/BE7CeweeRex8 ---- kg+qz3hjdUn8vfjkrxVlJ9CRepSWrTtRshFpXyg62CE -/BEBI渁ǫFx="nv% )*OF[@m=\fy#>ࢴ u៬hL=W>a I[.\qLmZ>mǂ8Kn袀 \ No newline at end of file +-> ssh-ed25519 2purlw k4nPD17tbxNpuXInJRyXlXaw6U3uaGG9A2CAGajRRCU +UPUFfJbAoOCPxlHBESXGJYLvOLiOOEo5N02fFSceQp0 +-> ssh-ed25519 +EkgOg 6jxkUgGyPS865LxZ7bk5TXoSaWuN+BJ47j5Z302EOwg +gI5GhYNTTscfedLdF3G1x7aNLOZhP/6k5uuQu6Rqe4U +-> ssh-ed25519 d3XP2A Ee6texItJ8y5NemnPE9ALiqSelb8d+az6e4FszXrIE4 +EajHuocZvAMBOEwzLo53wAWuOLI6QadXg5YcW3moXr0 +-> ssh-ed25519 3iWz6A uZ258Vb8bxiRqW490YomAwPKdT4wowDVpsL4i7q9b0s +0qzZfaeTnQfRvGfvUrrqPdGd3PcwIHd2aZ7cw5RFnMA +--- +ufpZ6AJwoUGZwixxX+kxTvhtcbRkDGF9QgrZXpZ4r8 +,=Rð0u= s"ߑd4"^*{vc&qh +đYݡ7 D`z +hfĺS^DvuU0Z+Xy?NQNLa \ No newline at end of file diff --git a/infrastructure/secrets/secrets.nix b/infrastructure/secrets/secrets.nix index 6364f895e..f8a094ac4 100644 --- a/infrastructure/secrets/secrets.nix +++ b/infrastructure/secrets/secrets.nix @@ -4,6 +4,7 @@ let owen = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF2sBTuqGoEXRWpBRqTBwZZPDdLGGJ0GQcuX5dfIZKb4 o@red-special"; epatters = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAKXx6wMJSeYKCHNmbyR803RQ72uto9uYsHhAPPWNl2D evan@epatters.org"; jmoggr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMiaHaeJ5PQL0mka/lY1yGXIs/bDK85uY1O3mLySnwHd j@jmoggr.com"; + kasbah = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1K/FB6dCjo1/xfddi9VoHEGchFo/bcz6v7SC7wAuFQ kaspar@topos"; catcolab-next-deployuser = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM7AYg1fZM0zMxb/BuZTSwK4O3ycUIHruApr1tKoO8nJ deployuser@next.catcolab.org"; in builtins.mapAttrs (_: publicKeys: { inherit publicKeys; }) ({ @@ -13,6 +14,7 @@ builtins.mapAttrs (_: publicKeys: { inherit publicKeys; }) ({ epatters jmoggr catcolab-next-deployuser + kasbah ]; "env.prod.age" = [ catcolab @@ -26,6 +28,7 @@ builtins.mapAttrs (_: publicKeys: { inherit publicKeys; }) ({ epatters jmoggr catcolab-next-deployuser + kasbah ]; "rclone.conf.prod.age" = [ catcolab diff --git a/infrastructure/tests/frontend.nix b/infrastructure/tests/frontend.nix new file mode 100644 index 000000000..c89aec098 --- /dev/null +++ b/infrastructure/tests/frontend.nix @@ -0,0 +1,52 @@ +{ + nixpkgs, + inputs, + self, + rustToolchain, + linuxSystem, +}: +nixpkgs.legacyPackages.${linuxSystem}.testers.runNixOSTest { + name = "Frontend Tests"; + + skipTypeCheck = true; + + nodes = { + catcolab_vm = + { pkgs, ... }: + { + imports = [ ../hosts/catcolab-vm ]; + + environment.systemPackages = with pkgs; [ + nodejs_24 + curl + ]; + }; + }; + + node.specialArgs = { + inherit inputs self rustToolchain; + }; + + testScript = '' + def dump_logs(machine, *units): + for u in units: + print(f"\n===== journal for {u} =====") + print(machine.succeed(f"journalctl -u {u} --no-pager")) + + catcolab_vm.start() + + status, test_output = catcolab_vm.execute( + # redirect stderr to stdout, otherwise error messages won't be included in test_output + "${self.packages.x86_64-linux.frontend-tests}/bin/frontend-tests 2>&1" + ) + + dump_logs(catcolab_vm, "automerge.service") + dump_logs(catcolab_vm, "backend.service") + + print("\n===== frontend tests output =====") + print(test_output) + + if status != 0: + raise Exception(f"Frontend tests failed with exit code {status}") + ''; +} diff --git a/infrastructure/tests/integration.nix b/infrastructure/tests/integration.nix deleted file mode 100644 index 405bac3fd..000000000 --- a/infrastructure/tests/integration.nix +++ /dev/null @@ -1,59 +0,0 @@ -# The backend relies on Firebase, so tests require VM internet access. Enable networking by running -# with --no-sandbox. -# Docs for nixos tests: https://nixos.org/manual/nixos/stable/index.html#sec-nixos-test-nodes -# (google and LLMs are useless) -{ - nixpkgs, - inputs, - self, - rustToolchain, - linuxSystem, -}: -nixpkgs.legacyPackages.${linuxSystem}.testers.runNixOSTest { - name = "Integration Tests"; - - skipTypeCheck = true; - - nodes = { - catcolab = import ../hosts/catcolab-vm; - }; - - node.specialArgs = { - inherit inputs self rustToolchain; - }; - - # NOTE: This only checks if the services "start" from systemds perspective, not if they are not - # failed immediately after starting... - testScript = '' - def dump_logs(machine, *units): - for u in units: - print(f"\n===== journal for {u} =====") - print(machine.succeed(f"journalctl -u {u} --no-pager")) - - def test_service(machine, service): - try: - machine.wait_for_unit(service) - except: - dump_logs(machine, service) - raise - - def test_oneshot_service(machine, service): - try: - machine.wait_until_succeeds( - f"test $(systemctl is-active {service}) = inactive" - ) - except: - dump_logs(machine, service) - raise - - test_oneshot_service(catcolab, "database-setup.service") - test_oneshot_service(catcolab, "migrations.service") - - test_service(catcolab, "automerge.service"); - test_service(catcolab, "backend.service"); - test_service(catcolab, "caddy.service"); - - catcolab.start_job("backupdb.service") - test_oneshot_service(catcolab, "backupdb.service") - ''; -} diff --git a/packages/frontend/.env.production b/packages/frontend/.env.production deleted file mode 100644 index edb2cfe07..000000000 --- a/packages/frontend/.env.production +++ /dev/null @@ -1,14 +0,0 @@ -VITE_APP_TITLE="CatColab" - -VITE_SERVER_URL=https://backend.catcolab.org -VITE_AUTOMERGE_REPO_URL=wss://automerge.catcolab.org - -VITE_FIREBASE_OPTIONS='{ - "apiKey": "AIzaSyA9zb9RPri76DXB0Bnq3LZYa2u61l-K9oA", - "authDomain": "catcolab-prod.firebaseapp.com", - "projectId": "catcolab-prod", - "storageBucket": "catcolab-prod.firebasestorage.app", - "messagingSenderId": "192417001062", - "appId": "1:192417001062:web:9089adb3abceac686187a7", - "measurementId": "G-TQXNCW7D84" -}' diff --git a/packages/frontend/default.nix b/packages/frontend/default.nix index e430f19d0..e3e018b97 100644 --- a/packages/frontend/default.nix +++ b/packages/frontend/default.nix @@ -2,6 +2,7 @@ pkgs, inputs, self, + lib, ... }: let @@ -12,60 +13,165 @@ let pkgsUnstable = import inputs.nixpkgsUnstable { system = "x86_64-linux"; }; -in -pkgs.stdenv.mkDerivation { - pname = name; - version = version; - src = ./.; - - nativeBuildInputs = with pkgs; [ - pnpm_9.configHook - ]; - - buildInputs = with pkgs; [ - nodejs_24 - ]; - - # package.json expects notebook-types to be at ../notebook-types, we COULD modify the parent of the nix - # `build` directory, but this is technically unsupported. Instead we recreate part of the `packages` - # directory structure in a way familiar to pnpm. - unpackPhase = '' - mkdir -p ./catlog-wasm/dist/pkg-browser - cp -r ${self.packages.x86_64-linux.catlog-wasm-browser}/* ./catlog-wasm/dist/pkg-browser/ - - mkdir -p ./backend/pkg - cp -r ${self.packages.x86_64-linux.catcolabApi}/* ./backend/pkg/ - - mkdir ./frontend - cp -r $src/* ./frontend - cp -r $src/.* ./frontend - - cd ./frontend - ''; - - installPhase = '' - # The catcolab-api package is a bit odd since it's a built/generated dependency that's tracked by - # git. Fortunately it shares dependencies with the frontend, so we can just copy them. - mkdir -p ../backend/pkg/node_modules - cp -Lr node_modules/@qubit-rs ../backend/pkg/node_modules/ - cp -Lr node_modules/typescript ../backend/pkg/node_modules/ - - npm run build:nix - - mkdir -p $out - cp -r ./dist/* $out - ''; - - pnpmDeps = pkgsUnstable.pnpm_9.fetchDeps { - pname = name; - - fetcherVersion = 2; - src = ./.; - - # See README.md - # hash = ""; - hash = "sha256-krMxJvNarhwZK8O08RonIwpzaTlmjT2ToWINe+tm9n8="; + + commonAttrs = { + version = version; + # Filter source to only include packages needed for frontend build + # This prevents unnecessary rebuilds when unrelated files change + src = lib.fileset.toSource { + root = ../../.; + fileset = lib.fileset.unions [ + ../../.npmrc + ../../pnpm-workspace.yaml + ../../pnpm-lock.yaml + ../../packages/frontend + ../../packages/ui-components + ../../packages/notebook-types + ../../packages/backend/pkg + ]; + }; + + nativeBuildInputs = with pkgs; [ + pnpm_9.configHook + ]; + + buildInputs = with pkgs; [ + nodejs_24 + ]; + + pnpmDeps = pkgsUnstable.pnpm_9.fetchDeps { + pname = name; + fetcherVersion = 2; + # Only includes package.json and pnpm-lock.yaml files to ensure consistent hashing in different + # environments + src = lib.fileset.toSource { + root = ../../.; + fileset = lib.fileset.unions [ + ../../.npmrc + ../../pnpm-workspace.yaml + ../../pnpm-lock.yaml + ../../packages/frontend/package.json + ../../packages/frontend/pnpm-lock.yaml + ../../packages/ui-components/package.json + ../../packages/ui-components/pnpm-lock.yaml + ../../packages/notebook-types/package.json + ../../packages/notebook-types/pnpm-lock.yaml + ../../packages/backend/pkg/package.json + ../../packages/backend/pkg/pnpm-lock.yaml + ]; + }; + # See README.md + # hash = ""; + hash = "sha256-FqjdtE/OgmV+aYGh1AijQqvfg9/UQC/dClJRTLyGxCE="; + }; }; - meta.mainProgram = name; + package = pkgs.stdenv.mkDerivation ( + commonAttrs + // { + pname = name; + + buildPhase = '' + # Set up catlog-wasm before TypeScript build needs it + mkdir -p packages/catlog-wasm/dist/pkg-browser + cp -r ${self.packages.x86_64-linux.catlog-wasm-browser}/* packages/catlog-wasm/dist/pkg-browser/ + + cd packages/frontend + # Build with development mode to use .env.development configuration + npm run build:nix -- --mode development + cd - + ''; + + installPhase = '' + mkdir -p $out + cp -r packages/frontend/dist/* $out + ''; + } + ); + + tests = pkgs.stdenv.mkDerivation ( + commonAttrs + // { + pname = "${name}-tests"; + + nativeBuildInputs = commonAttrs.nativeBuildInputs ++ [ pkgs.makeWrapper ]; + + # Disable parts of the fixup phase. This is to improve performance, otherwise it would process + # all of node_modules for no benefit. + dontPatchELF = true; + dontStrip = true; + dontPatchShebangs = true; + + installPhase = '' + # for vitest to work we need to basically recreate the development environment. We acheive this + # by setting of up a copy of the packages structure. + mkdir -p $out/packages + + mkdir -p $out/packages/catlog-wasm/dist/pkg-browser + cp -r ${self.packages.x86_64-linux.catlog-wasm-browser}/* $out/packages/catlog-wasm/dist/pkg-browser/ + + cp -r packages/backend $out/packages/ + cp -r packages/frontend $out/packages/ + cp -r packages/ui-components $out/packages/ + cp -r packages/notebook-types $out/packages/ + + mkdir -p $out/bin + # Wrapper script to load environment variables and wait for backend to become available + cat > $out/bin/.${name}-tests-unwrapped <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + + cd "@OUT@/packages/frontend" + + if [ ! -f .env.development ]; then + echo "Error: .env.development file not found in $out/packages/frontend" >&2 + exit 1 + fi + + # Export all environment variables from .env.development + set -a + source .env.development + set +a + + # Wait for server to be available + echo "Waiting for backend at $VITE_SERVER_URL/status to be available..." + timeout=30 + elapsed=0 + + while [ $elapsed -lt $timeout ]; do + if response=$(curl -s "$VITE_SERVER_URL/status" 2>/dev/null); then + if [ "$response" = "Running" ]; then + echo "Server is running!" + break + fi + fi + + if [ $elapsed -ge $timeout ]; then + echo "Error: Timeout waiting for server to be available after ''${timeout}s" >&2 + exit 1 + fi + + sleep 1 + elapsed=$((elapsed + 1)) + done + + npm run test:ci + EOF + + substituteInPlace $out/bin/.${name}-tests-unwrapped \ + --replace '@OUT@' "$out" + + chmod +x $out/bin/.${name}-tests-unwrapped + + # Wrap the script to ensure nodejs is in PATH + makeWrapper $out/bin/.${name}-tests-unwrapped $out/bin/${name}-tests \ + --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.nodejs_24 ]} + ''; + + meta.mainProgram = "${name}-tests"; + } + ); +in +{ + inherit package tests; } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index e7501991e..5162b3f34 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -16,8 +16,8 @@ "lint": "biome lint --write && biome check --write", "ci": "biome ci", "doc": "typedoc --entryPointStrategy expand ./src", - "test": "vitest --mode development", - "test:no-backend": "vitest --mode development --exclude 'src/api/*'" + "test": "vitest --mode=development", + "test:ci": "vitest --mode=development --cache=false --configLoader=runner --run" }, "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.3.0", @@ -52,6 +52,7 @@ "fast-json-patch": "^3.1.1", "firebase": "^10.14.0", "js-file-download": "^0.4.12", + "jsdom": "^27.0.1", "katex": "^0.16.22", "lucide-solid": "^0.471.0", "prosemirror-commands": "^1.7.1", diff --git a/packages/frontend/pnpm-lock.yaml b/packages/frontend/pnpm-lock.yaml index 0e714b26f..d76a27aa1 100644 --- a/packages/frontend/pnpm-lock.yaml +++ b/packages/frontend/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: js-file-download: specifier: ^0.4.12 version: 0.4.12 + jsdom: + specifier: ^27.0.1 + version: 27.0.1(postcss@8.4.47) katex: specifier: ^0.16.22 version: 0.16.22 @@ -194,7 +197,7 @@ importers: version: 3.5.0(vite@7.2.2(@types/node@24.10.0)(yaml@2.5.1)) vitest: specifier: ^4.0.8 - version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(yaml@2.5.1) + version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jsdom@27.0.1(postcss@8.4.47))(yaml@2.5.1) wasm-pack: specifier: ^0.13.1 version: 0.13.1 @@ -205,6 +208,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.7.3': + resolution: {integrity: sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.0.3': resolution: {integrity: sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA==} @@ -441,6 +453,40 @@ packages: peerDependencies: solid-js: ^1.8 + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.14': + resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -838,9 +884,6 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1180,8 +1223,8 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1273,6 +1316,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -1307,6 +1354,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -1333,6 +1384,9 @@ packages: base-x@4.0.1: resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1457,14 +1511,26 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@5.3.1: + resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==} + engines: {node: '>=20'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -1483,6 +1549,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -1531,6 +1600,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1746,6 +1819,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} @@ -1755,6 +1832,18 @@ packages: http-parser-js@0.5.8: resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -1820,6 +1909,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} @@ -1858,6 +1950,15 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsdom@27.0.1: + resolution: {integrity: sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==} + engines: {node: '>=20'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -1920,6 +2021,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2006,6 +2111,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -2269,6 +2377,9 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2398,6 +2509,10 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -2487,6 +2602,9 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -2494,6 +2612,13 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2619,6 +2744,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -2640,6 +2768,13 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -2648,6 +2783,14 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2910,6 +3053,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wasm-pack@0.13.1: resolution: {integrity: sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==} hasBin: true @@ -2917,6 +3064,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} engines: {node: '>=0.8.0'} @@ -2925,6 +3076,18 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2970,6 +3133,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xstate@5.20.1: resolution: {integrity: sha512-i9ZpNnm/XhCOMUxae1suT8PjYNTStZWbhmuKt4xeTPaYG5TS0Fz0i+Ka5yxoNPpaHW3VW6JIowrwFgSTZONxig==} @@ -3019,6 +3189,24 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@asamuzakjp/css-color@4.0.5': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.7.3': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.0.3': dependencies: '@atlaskit/pragmatic-drag-and-drop': 1.3.1 @@ -3080,7 +3268,7 @@ snapshots: '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/compat-data@7.25.7': {} @@ -3097,7 +3285,7 @@ snapshots: '@babel/traverse': 7.25.7 '@babel/types': 7.25.7 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3165,7 +3353,7 @@ snapshots: '@babel/helper-validator-identifier': 7.25.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/parser@7.25.7': dependencies: @@ -3193,7 +3381,7 @@ snapshots: '@babel/parser': 7.25.7 '@babel/template': 7.25.7 '@babel/types': 7.25.7 - debug: 4.4.1 + debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3308,6 +3496,30 @@ snapshots: '@floating-ui/dom': 1.6.11 solid-js: 1.9.10 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.4.47)': + dependencies: + postcss: 8.4.47 + + '@csstools/css-tokenizer@3.0.4': {} + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -3758,21 +3970,19 @@ snapshots: '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jupyter/ydoc@3.0.1': dependencies: @@ -4178,9 +4388,10 @@ snapshots: dependencies: '@babel/types': 7.25.7 - '@types/chai@5.2.2': + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 '@types/debug@4.1.12': dependencies: @@ -4231,7 +4442,7 @@ snapshots: '@vitest/expect@4.0.8': dependencies: '@standard-schema/spec': 1.0.0 - '@types/chai': 5.2.2 + '@types/chai': 5.2.3 '@vitest/spy': 4.0.8 '@vitest/utils': 4.0.8 chai: 6.2.0 @@ -4275,6 +4486,8 @@ snapshots: acorn@8.12.1: {} + agent-base@7.1.4: {} + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -4305,6 +4518,8 @@ snapshots: argparse@2.0.1: {} + assertion-error@2.0.1: {} + astring@1.9.0: {} axios@0.26.1: @@ -4335,6 +4550,10 @@ snapshots: base-x@4.0.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} binary-install@1.1.0: @@ -4480,10 +4699,28 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + cssesc@3.0.0: {} + cssstyle@5.3.1(postcss@8.4.47): + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.4.47) + css-tree: 3.1.0 + transitivePeerDependencies: + - postcss + csstype@3.1.3: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -4492,6 +4729,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -4531,6 +4770,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + es-module-lexer@1.7.0: {} esast-util-from-estree@2.0.0: @@ -4918,12 +5159,34 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-entities@2.3.3: {} html-void-elements@3.0.0: {} http-parser-js@0.5.8: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + icss-replace-symbols@1.1.0: {} icss-utils@5.1.0(postcss@8.4.47): @@ -4972,6 +5235,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@3.0.2: dependencies: '@types/estree': 1.0.6 @@ -5007,6 +5272,34 @@ snapshots: js-tokens@4.0.0: {} + jsdom@27.0.1(postcss@8.4.47): + dependencies: + '@asamuzakjp/dom-selector': 6.7.3 + cssstyle: 5.3.1(postcss@8.4.47) + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - postcss + - supports-color + - utf-8-validate + jsesc@3.0.2: {} json-schema-compare@0.2.2: @@ -5055,6 +5348,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5294,6 +5589,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} + mdurl@2.0.0: {} merge-anything@5.1.7: @@ -5795,6 +6092,10 @@ snapshots: dependencies: entities: 4.5.0 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + path-browserify@1.0.1: {} path-is-absolute@1.0.1: {} @@ -5948,6 +6249,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@2.3.1: {} + querystringify@2.2.0: {} raf-schd@4.0.3: {} @@ -6115,12 +6418,20 @@ snapshots: rope-sequence@1.3.4: {} + rrweb-cssom@0.8.0: {} + sade@1.8.1: dependencies: mri: 1.2.0 safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@6.3.1: {} seroval-plugins@1.3.2(seroval@1.3.2): @@ -6248,6 +6559,8 @@ snapshots: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -6270,12 +6583,26 @@ snapshots: tinyrainbow@3.0.3: {} + tldts-core@7.0.17: {} + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -6414,7 +6741,7 @@ snapshots: dependencies: browserslist: 4.24.0 escalade: 3.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 url-parse@1.5.10: dependencies: @@ -6516,7 +6843,7 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@24.10.0)(yaml@2.5.1) - vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(yaml@2.5.1): + vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jsdom@27.0.1(postcss@8.4.47))(yaml@2.5.1): dependencies: '@vitest/expect': 4.0.8 '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.0)(yaml@2.5.1)) @@ -6541,6 +6868,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.0 + jsdom: 27.0.1(postcss@8.4.47) transitivePeerDependencies: - jiti - less @@ -6557,6 +6885,10 @@ snapshots: w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wasm-pack@0.13.1: dependencies: binary-install: 1.1.0 @@ -6565,6 +6897,8 @@ snapshots: web-namespaces@2.0.1: {} + webidl-conversions@8.0.0: {} + websocket-driver@0.7.4: dependencies: http-parser-js: 0.5.8 @@ -6573,6 +6907,17 @@ snapshots: websocket-extensions@0.1.4: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6600,6 +6945,10 @@ snapshots: ws@8.18.3: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xstate@5.20.1: {} y-protocols@1.0.6(yjs@13.6.20): diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 097c39320..e78f30085 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import mdx from "@mdx-js/rollup"; import rehypeKatex from "rehype-katex"; import remarkMath from "remark-math"; @@ -7,6 +8,11 @@ import { defineConfig } from "vite"; import solid from "vite-plugin-solid"; import wasm from "vite-plugin-wasm"; +// __dirname is not available in ES modules. The test:ci script uses --configLoader=runner +// (required for readonly (Nix) environments), which runs Vite in ESM mode. +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + export default defineConfig({ plugins: [ wasm(),