From c9d450efa7c5d0bd9129da90f5a56661965f69de Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 29 Jun 2026 12:49:07 -0500 Subject: [PATCH 1/4] feat: run the Boxel stack in Claude Code on the web Add .devcontainer/claude-web-setup.sh, a provisioning script to point the claude.ai/code cloud environment's Setup Script at. The cloud VM runs the whole stack on localhost via the standard `mise run dev` (no edge, no proxy/URL-rewriting needed); the script installs the toolchain + deps, builds the boxel-ui/icons addons (the host vite build needs the per-icon modules), provisions the local dev cert, and clones the skills realm. Catalog is skipped at runtime (SKIP_CATALOG=true mise run dev) to fit the ~16GB VM. Make Synapse start on the cloud VM (a headless root host): pass UID/GID=0 so the image doesn't drop to uid 991 and fail to write the root-owned config, and bind listeners to 0.0.0.0 when the kernel has no IPv6 (the default "::" bind dies otherwise). Both are gated on the actual condition, so normal dev hosts are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .devcontainer/claude-web-setup.sh | 64 ++++++++++++++++++++++++ packages/matrix/support/synapse/index.ts | 40 +++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 .devcontainer/claude-web-setup.sh diff --git a/.devcontainer/claude-web-setup.sh b/.devcontainer/claude-web-setup.sh new file mode 100755 index 0000000000..4b1c4867b2 --- /dev/null +++ b/.devcontainer/claude-web-setup.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Provisioning for running the Boxel stack in "Claude Code on the web" +# (claude.ai/code). Point the cloud environment's *Setup Script* at this file. +# +# The cloud VM runs the whole stack on localhost, so this just uses the repo's +# STANDARD dev tooling (`mise run dev`): the realm is at https://localhost:4201, +# the migration-seeded permissions already match that localhost default, and +# the worker/prerender reach it directly. No reverse proxy, TLS shim, or URL +# rewriting is needed — it's normal local dev, provisioned for a headless +# root cloud VM (see the synapse root/no-IPv6 handling in +# packages/matrix/support/synapse/index.ts). +# +# This script only PROVISIONS (deps + dev cert + source realms). Start the +# stack PER SESSION (services don't persist in the cached snapshot) with: +# +# SKIP_CATALOG=true mise run dev +# +# (or a lighter variant: `dev-without-matrix`, `dev-minimal`). +# +# Cloud environment settings to set in the claude.ai/code UI: +# - Network access: "Full" (or a custom allowlist) — needed for OpenRouter, +# GitHub, Docker Hub, and the icon CDN (boxel-icons.boxel.ai). +# - RAM ceiling is ~16 GB, so the catalog realm (by far the heaviest to index, +# ~1000+ files) is skipped via SKIP_CATALOG to stay within budget. +set -euo pipefail + +# Toolchain — mise pins the exact node/pnpm/ts-node from .mise.toml. +if ! command -v mise >/dev/null 2>&1; then + curl https://mise.run | MISE_INSTALL_PATH="$HOME/.local/bin/mise" sh + export PATH="$HOME/.local/bin:$PATH" +fi +eval "$(mise activate bash)" +mise trust +mise install + +# Dependencies. +mise exec -- pnpm install --frozen-lockfile + +# Build the boxel-icons + boxel-ui addons (in dependency order). The host app's +# vite build imports per-icon modules from @cardstack/boxel-icons/dist, which +# `pnpm install` does not produce — without this the host fails to build with +# "Cannot find module '@cardstack/boxel-icons/...'" and never serves. +mise run build:ui + +# Local-dev TLS cert: standard dev serves HTTPS on localhost and env-vars.sh +# treats the cert as mandatory. Provisioning it here also lets Node (via +# NODE_EXTRA_CA_CERTS, set by env-vars.sh) and the prerender's headless Chrome +# trust https://localhost — and because localhost IS an https-loopback, +# browser-manager.ts auto-adds --ignore-certificate-errors (no extra config). +mise run infra:ensure-dev-cert + +# Source realms live in separate repos; clone over HTTPS (no SSH key in the VM). +# Catalog is intentionally NOT cloned here — it's skipped at runtime to fit the +# memory budget. Add `pnpm --dir=packages/catalog catalog:setup` if you need it. +git config --global url."https://github.com/".insteadOf "git@github.com:" +mise exec -- pnpm --dir=packages/skills-realm skills:setup + +# Note: the first `mise run dev` pulls the Synapse/Postgres Docker images; the +# cloud snapshot caches them so later sessions start faster. + +echo "" +echo "Provisioning complete. Start the stack (catalog skipped) with:" +echo " SKIP_CATALOG=true mise run dev" +echo "Realm: https://localhost:4201 Host: https://localhost:4200" diff --git a/packages/matrix/support/synapse/index.ts b/packages/matrix/support/synapse/index.ts index 8fe96c40d6..4a20af1876 100644 --- a/packages/matrix/support/synapse/index.ts +++ b/packages/matrix/support/synapse/index.ts @@ -23,6 +23,24 @@ import { export const SYNAPSE_IP_ADDRESS = '172.20.0.5'; export const SYNAPSE_PORT = 8008; +// Synapse's listeners bind to "::" (IPv6 dual-stack) by default. Hosts whose +// kernel lacks IPv6 (some minimal cloud VMs / containers) can't bind it and +// synapse dies at startup with "Address family not supported by protocol". We +// detect that here so the generated config can fall back to IPv4-only binding. +function hostHasIPv6(): boolean { + let interfaces = os.networkInterfaces(); + for (let name of Object.keys(interfaces)) { + for (let info of interfaces[name] ?? []) { + // Node has reported `family` as both the string 'IPv6' and the number 6 + // across versions; accept either. + if (info.family === 'IPv6' || (info.family as unknown) === 6) { + return true; + } + } + } + return false; +} + const registrationSecretFile = path.resolve( path.join(import.meta.dirname, '..', '..', 'registration_secret.txt'), ); @@ -242,6 +260,21 @@ export async function synapseStart( port: hostPort, publicBaseUrl: `http://localhost:${hostPort}`, }); + // On a host without IPv6, rewrite the generated config's listeners to bind + // IPv4 only — synapse is reached via localhost:8008 in dev regardless, so + // dropping the dual-stack "::" bind is transparent there but lets synapse + // start at all. Hosts with IPv6 keep the template's "::" untouched. + if (!hostHasIPv6()) { + let hsYaml = path.join(synCfg.configDir, 'homeserver.yaml'); + let contents = await fse.readFile(hsYaml, 'utf8'); + let patched = contents.replace( + /bind_addresses:\s*\[\s*"::"\s*\]/g, + 'bind_addresses: ["0.0.0.0"]', + ); + if (patched !== contents) { + await fse.writeFile(hsYaml, patched); + } + } containerName = opts?.containerName || (isEnvironmentMode() @@ -262,6 +295,13 @@ export async function synapseStart( '-e', 'PYTHONPATH=/custom/modules', ]; + // When the host runs as root (e.g. the Claude-web cloud VM), the synapse + // image would otherwise drop privileges to its default uid 991, which + // cannot write the root-owned config dir mounted at /data. Telling the + // image to stay as root (UID/GID=0) keeps it able to create media_store. + if (process.getuid?.() === 0) { + dockerParams.push('-e', 'UID=0', '-e', 'GID=0'); + } if (useDynamicHostPort) { // In dynamic-host-port mode multiple harnesses may run concurrently, so // we must not claim the shared fixed Synapse container IP. From 801d38cc31831cb340058169972f1fa01c2bcae6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:29:16 +0000 Subject: [PATCH 2/4] Adapt the Claude-web stack setup to a headless root cloud VM Build on the base setup: install mkcert (the dev-cert step requires it), write a combined CA bundle (agent-proxy CAs + mkcert rootCA) so Node trusts both the proxy for outbound HTTPS and the mkcert leaf over loopback, and add a per-session start script. The start script runs `mise run dev-all` (the headless VM has no second terminal for the host the prerender gates on), starts the Docker daemon, points NODE_EXTRA_CA_CERTS at the combined bundle, and registers Matrix users on the fresh Synapse before the stack boots so the realm-server logs in cleanly. SKIP_CATALOG fits the memory budget and SKIP_BOXEL_HOMEPAGE skips the realm whose content repo this VM can't clone. Co-Authored-By: Claude Opus 4.8 --- .devcontainer/claude-web-setup.sh | 49 ++++++++++++++++++---- .devcontainer/claude-web-start.sh | 70 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 7 deletions(-) create mode 100755 .devcontainer/claude-web-start.sh diff --git a/.devcontainer/claude-web-setup.sh b/.devcontainer/claude-web-setup.sh index 4b1c4867b2..ff2cb4beba 100755 --- a/.devcontainer/claude-web-setup.sh +++ b/.devcontainer/claude-web-setup.sh @@ -10,18 +10,26 @@ # root cloud VM (see the synapse root/no-IPv6 handling in # packages/matrix/support/synapse/index.ts). # -# This script only PROVISIONS (deps + dev cert + source realms). Start the -# stack PER SESSION (services don't persist in the cached snapshot) with: +# This script only PROVISIONS (deps + mkcert + dev cert + CA bundle + source +# realms). Start the stack PER SESSION (services don't persist in the cached +# snapshot) with the companion start script, which sets the env vars this +# environment needs and registers Matrix users on a fresh Synapse: # -# SKIP_CATALOG=true mise run dev +# .devcontainer/claude-web-start.sh # -# (or a lighter variant: `dev-without-matrix`, `dev-minimal`). +# It runs `mise run dev-all` (NOT `mise run dev`): the cloud VM is headless, so +# the host app must run in-process here. `dev` starts only the backend and +# leaves the host to a second terminal that this environment doesn't have — +# the prerender then waits forever for https://localhost:4200 and the whole +# stack fails. `dev-all` brings up the host first, then the same backend. # # Cloud environment settings to set in the claude.ai/code UI: # - Network access: "Full" (or a custom allowlist) — needed for OpenRouter, # GitHub, Docker Hub, and the icon CDN (boxel-icons.boxel.ai). # - RAM ceiling is ~16 GB, so the catalog realm (by far the heaviest to index, -# ~1000+ files) is skipped via SKIP_CATALOG to stay within budget. +# ~1000+ files) is skipped via SKIP_CATALOG to stay within budget. The +# boxel-homepage realm lives in a private repo this VM can't clone, so it's +# skipped too (SKIP_BOXEL_HOMEPAGE) — both are set by the start script. set -euo pipefail # Toolchain — mise pins the exact node/pnpm/ts-node from .mise.toml. @@ -42,6 +50,16 @@ mise exec -- pnpm install --frozen-lockfile # "Cannot find module '@cardstack/boxel-icons/...'" and never serves. mise run build:ui +# mkcert provisions the local-dev CA + leaf cert; infra:ensure-dev-cert fails +# hard if it's missing. The base cloud image doesn't ship it, so install it +# (and libnss3-tools, which mkcert -install needs to write the NSS trust DB). +if ! command -v mkcert >/dev/null 2>&1; then + SUDO="" + [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && SUDO="sudo" + $SUDO apt-get update -y + $SUDO apt-get install -y mkcert libnss3-tools +fi + # Local-dev TLS cert: standard dev serves HTTPS on localhost and env-vars.sh # treats the cert as mandatory. Provisioning it here also lets Node (via # NODE_EXTRA_CA_CERTS, set by env-vars.sh) and the prerender's headless Chrome @@ -49,6 +67,23 @@ mise run build:ui # browser-manager.ts auto-adds --ignore-certificate-errors (no extra config). mise run infra:ensure-dev-cert +# Combined CA bundle. This cloud environment routes outbound HTTPS through an +# agent proxy and pre-sets NODE_EXTRA_CA_CERTS to the proxy's CA bundle. Node +# reads NODE_EXTRA_CA_CERTS as a SINGLE file (not a list), and env-vars.sh +# only points it at mkcert's rootCA when it's unset — so the proxy value wins +# and Node never trusts the mkcert leaf. The realm-server's startup fetch of +# the host (https://localhost:4200) then fails with +# UNABLE_TO_VERIFY_LEAF_SIGNATURE and it crash-loops. Concatenate the proxy +# bundle and mkcert's rootCA into one file so Node trusts BOTH the proxy +# (outbound) and the local leaf (loopback); the start script exports +# NODE_EXTRA_CA_CERTS at it. No-op when the env doesn't pre-set a proxy CA. +if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -f "${NODE_EXTRA_CA_CERTS}" ]; then + CAROOT="$(mkcert -CAROOT)" + COMBINED="$HOME/.local/share/boxel/dev-certs/combined-ca.pem" + cat "${NODE_EXTRA_CA_CERTS}" "${CAROOT}/rootCA.pem" > "$COMBINED" + echo "Wrote combined CA bundle (proxy + mkcert) to $COMBINED" +fi + # Source realms live in separate repos; clone over HTTPS (no SSH key in the VM). # Catalog is intentionally NOT cloned here — it's skipped at runtime to fit the # memory budget. Add `pnpm --dir=packages/catalog catalog:setup` if you need it. @@ -59,6 +94,6 @@ mise exec -- pnpm --dir=packages/skills-realm skills:setup # cloud snapshot caches them so later sessions start faster. echo "" -echo "Provisioning complete. Start the stack (catalog skipped) with:" -echo " SKIP_CATALOG=true mise run dev" +echo "Provisioning complete. Start the stack with:" +echo " .devcontainer/claude-web-start.sh" echo "Realm: https://localhost:4201 Host: https://localhost:4200" diff --git a/.devcontainer/claude-web-start.sh b/.devcontainer/claude-web-start.sh new file mode 100755 index 0000000000..2bc84d9bca --- /dev/null +++ b/.devcontainer/claude-web-start.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Per-session start for the Boxel stack in "Claude Code on the web". Run after +# .devcontainer/claude-web-setup.sh has provisioned the snapshot. Services do +# not persist in the cached snapshot, so this runs every session. +# +# What this handles that plain `mise run dev` does not, in this environment: +# - dev-all, not dev: the VM is headless, so the host app must run in-process +# (see the note in claude-web-setup.sh). +# - Docker: the daemon isn't running at session start; bring it up so the +# Synapse / Postgres / SMTP containers can launch. +# - CA bundle: point Node at the combined proxy+mkcert bundle so the +# realm-server can verify the host's mkcert leaf over loopback while still +# trusting the agent proxy for outbound HTTPS (see claude-web-setup.sh). +# - Matrix users: standard dev assumes the realm/bot users are already +# registered (full-reset does it). On this fresh Synapse they are not, so +# the realm-server's Matrix login 403s and it runs without broadcasting. +# ensure-synapse only auto-registers in environment mode, so do it here — +# BEFORE the stack boots, so the realm-server logs in cleanly. The +# registration script is idempotent (skips users that already exist). +# - Chromium sandbox: the prerender's headless Chrome can't sandbox as root, +# so PUPPETEER_DISABLE_SANDBOX makes its standby probe pass. +# - SKIP_CATALOG / SKIP_BOXEL_HOMEPAGE: fit the memory budget and skip the +# realm whose content repo this VM can't clone. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +export PATH="$HOME/.local/bin:$PATH" +eval "$(mise activate bash)" + +# Docker daemon: start it if the socket isn't responding. Containers and their +# images are cached in the snapshot, but the daemon process is not. +if ! docker info >/dev/null 2>&1; then + echo "[start] Starting Docker daemon…" + (dockerd >/tmp/dockerd.log 2>&1 &) + for _ in $(seq 1 30); do + docker info >/dev/null 2>&1 && break + sleep 1 + done + docker info >/dev/null 2>&1 || { echo "[start] Docker failed to start; see /tmp/dockerd.log" >&2; exit 1; } +fi + +# Trust both the agent proxy CA (outbound) and the mkcert leaf (loopback). +COMBINED="$HOME/.local/share/boxel/dev-certs/combined-ca.pem" +if [ -f "$COMBINED" ]; then + export NODE_EXTRA_CA_CERTS="$COMBINED" +fi + +# Register Matrix users on a fresh Synapse, once, before the stack boots, so +# the realm-server logs in cleanly instead of caching a failed session. +# register-all needs BOTH the Postgres container (it gates on `pg_isready`) +# and Synapse, so bring both up first; dev-all's own start:pg / start:matrix +# then see them already running and move on. +echo "[start] Ensuring Postgres + Synapse are up for Matrix user registration…" +mise run infra:ensure-pg +mise run infra:start-synapse +for _ in $(seq 1 60); do + curl -sf -o /dev/null --max-time 5 http://localhost:8008/_matrix/client/versions && break + sleep 2 +done +echo "[start] Registering Matrix users (idempotent)…" +mise exec -- pnpm --dir=packages/matrix register-all || true + +echo "[start] Launching the stack (mise run dev-all)…" +exec env \ + SKIP_CATALOG=true \ + SKIP_BOXEL_HOMEPAGE=true \ + PUPPETEER_DISABLE_SANDBOX=true \ + mise run dev-all From b7540822d9a2ebfe483040386c8be334d3f08336 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:29:29 +0000 Subject: [PATCH 3/4] Restore realm index from CI cache on Claude-web startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add .devcontainer/claude-web-import-index.sh and wire it into the start script so a cold stack restores the CI-built index instead of re-rendering every card (the live prerender index takes minutes; the SQL restore takes seconds). CI's cache-index job already dumps boxel_index / realm_versions / realm_meta and uploads the boxel-index-cache artifact. The existing scripts/import-cached-index.sh relies on the gh CLI and is only wired into environment mode; this cloud session has neither (raw api.github.com is blocked — only the GitHub MCP integration can read Actions). So the new importer reads from a local cache file (default ~/.local/share/boxel/index-cache/boxel-index-cache.sql.gz), with a gh download fallback for devs who have it. It skips when the DB is already warm, migrates the schema, truncates the three tables, and restores the dump. No URL remap is needed: the cache stores https://localhost:4201 URLs, which match the standard-dev origin. When the import succeeds the start script boots dev-all with REALM_SERVER_FULL_INDEX_ON_STARTUP=false so the realm-server trusts the imported rows. Co-Authored-By: Claude Opus 4.8 --- .devcontainer/claude-web-import-index.sh | 79 ++++++++++++++++++++++++ .devcontainer/claude-web-start.sh | 9 +++ 2 files changed, 88 insertions(+) create mode 100755 .devcontainer/claude-web-import-index.sh diff --git a/.devcontainer/claude-web-import-index.sh b/.devcontainer/claude-web-import-index.sh new file mode 100755 index 0000000000..cb9b7de783 --- /dev/null +++ b/.devcontainer/claude-web-import-index.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Restore the realm index from a CI-built cache instead of indexing live. +# +# CI's `cache-index` job (.github/workflows/ci.yaml) indexes every realm and +# uploads a `pg_dump --data-only` of boxel_index / realm_versions / realm_meta +# as the `boxel-index-cache` artifact. Importing it turns the multi-minute +# prerender indexing into a seconds-long SQL restore. +# +# This is the gh-free sibling of scripts/import-cached-index.sh: this cloud +# session cannot reach api.github.com directly (it 403s — only the Claude +# GitHub MCP integration can read Actions), so this script imports from a +# LOCAL cache file rather than calling `gh run download`. A Claude session +# fetches the artifact via the Actions API (MCP) and drops it at the default +# path below; `gh` is still used as a fallback for devs who have it. +# +# Exit 0 = index restored (caller should boot with +# REALM_SERVER_FULL_INDEX_ON_STARTUP=false). +# Exit 1 = nothing imported (DB already warm, no cache, or import failed); +# caller should let the realm-server index live. +set -uo pipefail + +REPO="cardstack/boxel" +DB_NAME="${PGDATABASE:-boxel}" +CACHE_FILE="${BOXEL_INDEX_CACHE_FILE:-$HOME/.local/share/boxel/index-cache/boxel-index-cache.sql.gz}" + +# Already warm? The realm-server persists its index in boxel-pg; if the volume +# survived from a previous session there's nothing to restore. +ROW_COUNT=$(docker exec boxel-pg psql -U postgres -d "$DB_NAME" -tAc \ + "SELECT COUNT(*) FROM realm_versions" 2>/dev/null) || ROW_COUNT="" +if [ -n "$ROW_COUNT" ] && [ "$ROW_COUNT" -gt 0 ] 2>/dev/null; then + echo "[index-cache] DB already has index data ($ROW_COUNT realm versions); skipping import." + exit 1 +fi + +# Fall back to `gh` when a local cache file isn't present and the CLI exists. +if [ ! -f "$CACHE_FILE" ] && command -v gh >/dev/null 2>&1; then + RUN_ID=$(gh run list -w ci.yaml -b main -s success -L 1 \ + --json databaseId -q '.[0].databaseId' -R "$REPO" 2>/dev/null) || RUN_ID="" + if [ -n "$RUN_ID" ]; then + echo "[index-cache] Downloading cache from CI run $RUN_ID via gh…" + mkdir -p "$(dirname "$CACHE_FILE")" + gh run download "$RUN_ID" -n boxel-index-cache \ + -D "$(dirname "$CACHE_FILE")" -R "$REPO" 2>/dev/null || true + fi +fi + +if [ ! -f "$CACHE_FILE" ]; then + echo "[index-cache] No cache file at $CACHE_FILE (and no gh download); will index live." + echo "[index-cache] To use a cache, fetch the boxel-index-cache artifact from a" + echo "[index-cache] successful main CI run into that path (a Claude session can do" + echo "[index-cache] this via the GitHub Actions API; raw api.github.com is blocked here)." + exit 1 +fi + +# The data-only dump needs the schema to exist, so migrate first. Idempotent. +echo "[index-cache] Migrating schema before restore…" +if ! mise exec -- pnpm --dir=packages/realm-server migrate >/dev/null 2>&1; then + echo "[index-cache] Migration failed; will index live." >&2 + exit 1 +fi + +echo "[index-cache] Restoring index from $CACHE_FILE …" +docker exec boxel-pg psql -U postgres -d "$DB_NAME" --quiet --no-psqlrc -c \ + "TRUNCATE boxel_index, realm_versions, realm_meta" || { echo "[index-cache] truncate failed" >&2; exit 1; } + +# The cache stores https://localhost:4201/... URLs, which is exactly the +# standard-dev runtime origin — no remapping needed (unlike env mode). +if gunzip -c "$CACHE_FILE" \ + | docker exec -i boxel-pg psql -U postgres -d "$DB_NAME" --quiet --no-psqlrc -v ON_ERROR_STOP=1; then + RESTORED=$(docker exec boxel-pg psql -U postgres -d "$DB_NAME" -tAc \ + "SELECT COUNT(*) FROM realm_versions" 2>/dev/null) + echo "[index-cache] Restored ($RESTORED realm versions). Realm server will boot without a full index." + exit 0 +fi + +echo "[index-cache] Import failed; truncating partial data and indexing live." >&2 +docker exec boxel-pg psql -U postgres -d "$DB_NAME" --quiet --no-psqlrc -c \ + "TRUNCATE boxel_index, realm_versions, realm_meta" >/dev/null 2>&1 || true +exit 1 diff --git a/.devcontainer/claude-web-start.sh b/.devcontainer/claude-web-start.sh index 2bc84d9bca..2cc4cfa7fd 100755 --- a/.devcontainer/claude-web-start.sh +++ b/.devcontainer/claude-web-start.sh @@ -62,9 +62,18 @@ done echo "[start] Registering Matrix users (idempotent)…" mise exec -- pnpm --dir=packages/matrix register-all || true +# Restore the realm index from the CI cache if one's available, so the stack +# comes up without re-rendering every card. On success, tell the realm-server +# to trust the imported index instead of doing a full index on startup. +FULL_INDEX_FLAG="" +if "$REPO_ROOT/.devcontainer/claude-web-import-index.sh"; then + FULL_INDEX_FLAG="REALM_SERVER_FULL_INDEX_ON_STARTUP=false" +fi + echo "[start] Launching the stack (mise run dev-all)…" exec env \ SKIP_CATALOG=true \ SKIP_BOXEL_HOMEPAGE=true \ PUPPETEER_DISABLE_SANDBOX=true \ + ${FULL_INDEX_FLAG} \ mise run dev-all From 639000f918f3ef5155d299f39c4c386395dbe7d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 15:48:20 +0000 Subject: [PATCH 4/4] Vendor eslint-plugin-qunit-dom for restricted-network sessions This session's GitHub egress only allows cardstack/boxel, so the git-pinned eslint-plugin-qunit-dom dependency cannot be fetched from codeload.github.com. Vendor the exact pinned commit (d66c8419, v0.2.0, MIT) under .vendor/ and reference it via file: so pnpm install works. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jpPsbZYy9ZeRadczujY49 --- .../.eslint-doc-generatorrc.js | 8 + .vendor/eslint-plugin-qunit-dom/.eslintignore | 2 + .vendor/eslint-plugin-qunit-dom/.eslintrc.js | 33 +++ .../.github/renovate.json | 22 ++ .../.github/workflows/ci.yml | 82 ++++++ .../.github/workflows/release.yml | 22 ++ .vendor/eslint-plugin-qunit-dom/.gitignore | 116 +++++++++ .vendor/eslint-plugin-qunit-dom/.npmignore | 125 +++++++++ .../eslint-plugin-qunit-dom/.prettierrc.js | 9 + .../eslint-plugin-qunit-dom/.release-it.js | 16 ++ .vendor/eslint-plugin-qunit-dom/CHANGELOG.md | 60 +++++ .vendor/eslint-plugin-qunit-dom/LICENSE | 20 ++ .vendor/eslint-plugin-qunit-dom/README.md | 58 +++++ .../eslint-plugin-qunit-dom/configs.test.js | 19 ++ .vendor/eslint-plugin-qunit-dom/index.js | 43 +++ .vendor/eslint-plugin-qunit-dom/package.json | 41 +++ .../rules/no-checked-selector.js | 186 +++++++++++++ .../rules/no-checked-selector.md | 24 ++ .../rules/no-checked-selector.test.js | 119 +++++++++ .../rules/no-ok-find.js | 186 +++++++++++++ .../rules/no-ok-find.md | 32 +++ .../rules/no-ok-find.test.js | 244 ++++++++++++++++++ .../rules/require-assertion.js | 41 +++ .../rules/require-assertion.md | 23 ++ .../rules/require-assertion.test.js | 27 ++ packages/host/package.json | 2 +- pnpm-lock.yaml | 14 +- 27 files changed, 1564 insertions(+), 10 deletions(-) create mode 100644 .vendor/eslint-plugin-qunit-dom/.eslint-doc-generatorrc.js create mode 100644 .vendor/eslint-plugin-qunit-dom/.eslintignore create mode 100644 .vendor/eslint-plugin-qunit-dom/.eslintrc.js create mode 100644 .vendor/eslint-plugin-qunit-dom/.github/renovate.json create mode 100644 .vendor/eslint-plugin-qunit-dom/.github/workflows/ci.yml create mode 100644 .vendor/eslint-plugin-qunit-dom/.github/workflows/release.yml create mode 100644 .vendor/eslint-plugin-qunit-dom/.gitignore create mode 100644 .vendor/eslint-plugin-qunit-dom/.npmignore create mode 100644 .vendor/eslint-plugin-qunit-dom/.prettierrc.js create mode 100644 .vendor/eslint-plugin-qunit-dom/.release-it.js create mode 100644 .vendor/eslint-plugin-qunit-dom/CHANGELOG.md create mode 100644 .vendor/eslint-plugin-qunit-dom/LICENSE create mode 100644 .vendor/eslint-plugin-qunit-dom/README.md create mode 100644 .vendor/eslint-plugin-qunit-dom/configs.test.js create mode 100644 .vendor/eslint-plugin-qunit-dom/index.js create mode 100644 .vendor/eslint-plugin-qunit-dom/package.json create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.js create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.md create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.test.js create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-ok-find.js create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-ok-find.md create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/no-ok-find.test.js create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/require-assertion.js create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/require-assertion.md create mode 100644 .vendor/eslint-plugin-qunit-dom/rules/require-assertion.test.js diff --git a/.vendor/eslint-plugin-qunit-dom/.eslint-doc-generatorrc.js b/.vendor/eslint-plugin-qunit-dom/.eslint-doc-generatorrc.js new file mode 100644 index 0000000000..ef0419bb64 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.eslint-doc-generatorrc.js @@ -0,0 +1,8 @@ +/** @type {import('eslint-doc-generator').GenerateOptions} */ +const config = { + pathRuleDoc: 'rules/{name}.md', + ruleDocSectionInclude: ['Examples'], + ruleDocTitleFormat: 'name', +}; + +module.exports = config; diff --git a/.vendor/eslint-plugin-qunit-dom/.eslintignore b/.vendor/eslint-plugin-qunit-dom/.eslintignore new file mode 100644 index 0000000000..8e46a04e84 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.eslintignore @@ -0,0 +1,2 @@ +/coverage +!.* diff --git a/.vendor/eslint-plugin-qunit-dom/.eslintrc.js b/.vendor/eslint-plugin-qunit-dom/.eslintrc.js new file mode 100644 index 0000000000..8c9da35c4a --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.eslintrc.js @@ -0,0 +1,33 @@ +'use strict'; + +module.exports = { + root: true, + parserOptions: { + ecmaVersion: '2019', + }, + plugins: ['eslint-plugin', 'filenames', 'import', 'jest', 'node', 'prettier'], + extends: [ + 'eslint:recommended', + 'plugin:eslint-comments/recommended', + 'plugin:eslint-plugin/all', + 'plugin:jest/recommended', + 'plugin:jest/style', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:node/recommended', + 'prettier', + ], + env: { + node: true, + }, + rules: { + 'prettier/prettier': 'error', + }, + overrides: [ + { + // Test files: + files: ['tests/**/*.js'], + env: { jest: true }, + }, + ], +}; diff --git a/.vendor/eslint-plugin-qunit-dom/.github/renovate.json b/.vendor/eslint-plugin-qunit-dom/.github/renovate.json new file mode 100644 index 0000000000..915cd1f86c --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.github/renovate.json @@ -0,0 +1,22 @@ +{ + "extends": [ + "config:js-lib", + ":automergePatch", + ":automergeLinters", + ":automergeTesters", + ":dependencyDashboard", + ":maintainLockFilesWeekly", + ":semanticCommitsDisabled" + ], + "packageRules": [ + { + "matchCurrentVersion": ">= 1.0.0", + "updateTypes": ["minor"], + "automerge": true + }, + { + "packageNames": ["ember-cli", "ember-data", "ember-source"], + "separateMinorPatch": true + } + ] +} diff --git a/.vendor/eslint-plugin-qunit-dom/.github/workflows/ci.yml b/.vendor/eslint-plugin-qunit-dom/.github/workflows/ci.yml new file mode 100644 index 0000000000..61964ce128 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: CI + +on: + push: + branches: + - main + - 'v*' + pull_request: + +env: + FORCE_COLOR: 1 + PNPM_VERSION: 6.15.0 + +jobs: + lint: + name: Linting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + + - uses: pnpm/action-setup@v2.4.0 + with: + version: ${{ env.PNPM_VERSION }} + + - uses: actions/setup-node@v3 + with: + node-version: 14.x + cache: 'pnpm' + + - run: pnpm install + - run: pnpm lint + + test-node: + strategy: + matrix: + node-version: [12.x, 14.x, 16.x] + + name: Tests (Node.js ${{ matrix.node-version }}) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + + - uses: pnpm/action-setup@v2.4.0 + with: + version: ${{ env.PNPM_VERSION }} + + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - run: pnpm install + - run: pnpm test -- --coverage + + test-eslint: + strategy: + matrix: + eslint-version: [7.0.0] + + name: Tests (ESLint ${{ matrix.eslint-version }}) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + + - uses: pnpm/action-setup@v2.4.0 + with: + version: ${{ env.PNPM_VERSION }} + + - uses: actions/setup-node@v3 + with: + node-version: 14.x + # cache disabled because the extra `pnpm add` might conflict with the + # `test-node` job cache, and there is currently no way to influence + # the cache key + # cache: 'pnpm' + + - run: pnpm install + - run: pnpm add --save-dev eslint@${{ matrix.eslint-version }} + - run: pnpm test diff --git a/.vendor/eslint-plugin-qunit-dom/.github/workflows/release.yml b/.vendor/eslint-plugin-qunit-dom/.github/workflows/release.yml new file mode 100644 index 0000000000..6cdca82456 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + - uses: actions/setup-node@v3 + with: + node-version: 12.x + registry-url: 'https://registry.npmjs.org' + + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.vendor/eslint-plugin-qunit-dom/.gitignore b/.vendor/eslint-plugin-qunit-dom/.gitignore new file mode 100644 index 0000000000..1f22b9c26a --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.gitignore @@ -0,0 +1,116 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.vendor/eslint-plugin-qunit-dom/.npmignore b/.vendor/eslint-plugin-qunit-dom/.npmignore new file mode 100644 index 0000000000..ff5c686e34 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.npmignore @@ -0,0 +1,125 @@ +/.eslintignore +/.eslintrc.js +/.github/ +/.prettierrc.js +/.release-it.js +/pnpm-lock.yaml +/scripts/ +*.test.js + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.vendor/eslint-plugin-qunit-dom/.prettierrc.js b/.vendor/eslint-plugin-qunit-dom/.prettierrc.js new file mode 100644 index 0000000000..70d17558ef --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.prettierrc.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + printWidth: 100, + semi: true, + arrowParens: 'avoid', + singleQuote: true, + trailingComma: 'es5', +}; diff --git a/.vendor/eslint-plugin-qunit-dom/.release-it.js b/.vendor/eslint-plugin-qunit-dom/.release-it.js new file mode 100644 index 0000000000..1548e55324 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/.release-it.js @@ -0,0 +1,16 @@ +module.exports = { + plugins: { + 'release-it-lerna-changelog': { + infile: 'CHANGELOG.md', + }, + }, + git: { commitMessage: 'v${version}', tagName: 'v${version}' }, + github: { + release: true, + releaseName: 'v${version}', + tokenRef: 'GITHUB_AUTH', + }, + npm: { + publish: false, + }, +}; diff --git a/.vendor/eslint-plugin-qunit-dom/CHANGELOG.md b/.vendor/eslint-plugin-qunit-dom/CHANGELOG.md new file mode 100644 index 0000000000..a82a5e0503 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +## v0.2.0 (2021-09-16) + +#### :rocket: Enhancement + +- [#28](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/28) Reimplement `no-ok-find` rule ([@Turbo87](https://github.com/Turbo87)) + +#### Committers: 1 + +- Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) + +## v0.1.1 (2021-09-16) + +#### :rocket: Enhancement + +- [#27](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/27) Add `.npmignore` file ([@Turbo87](https://github.com/Turbo87)) + +#### Committers: 1 + +- Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) + +## v0.1.0 (2021-09-16) + +#### :boom: Breaking Change + +- [#13](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/13) Drop support for Node.js 10 ([@Turbo87](https://github.com/Turbo87)) + +#### :rocket: Enhancement + +- [#23](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/23) Export `recommended` ESLint config ([@Turbo87](https://github.com/Turbo87)) +- [#17](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/17) rules: Add metadata ([@Turbo87](https://github.com/Turbo87)) + +#### :bug: Bug Fix + +- [#22](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/22) Automatically export all rules ([@Turbo87](https://github.com/Turbo87)) + +#### :memo: Documentation + +- [#24](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/24) Add README and LICENSE files ([@Turbo87](https://github.com/Turbo87)) +- [#1](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/1) Add rule docs ([@Turbo87](https://github.com/Turbo87)) + +#### :house: Internal + +- [#25](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/25) Use `release-it` for releases ([@Turbo87](https://github.com/Turbo87)) +- [#26](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/26) ESLint: Enable for hidden files too ([@Turbo87](https://github.com/Turbo87)) +- [#19](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/19) Add local ESLint config and run it on CI ([@Turbo87](https://github.com/Turbo87)) +- [#18](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/18) rules: Use `messageId` instead of `message` ([@Turbo87](https://github.com/Turbo87)) +- [#16](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/16) no-checked-selector: Remove unused variable ([@Turbo87](https://github.com/Turbo87)) +- [#15](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/15) Adjust prettier config ([@Turbo87](https://github.com/Turbo87)) +- [#14](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/14) CI: Use `pnpm` v6 and cache dependencies ([@Turbo87](https://github.com/Turbo87)) +- [#12](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/12) Use pnpm instead of yarn ([@Turbo87](https://github.com/Turbo87)) +- [#10](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/10) CI: Split `release` job into dedicated workflow ([@Turbo87](https://github.com/Turbo87)) +- [#9](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/9) CI: Remove `cron` schedule ([@Turbo87](https://github.com/Turbo87)) +- [#3](https://github.com/Mainmatter/eslint-plugin-qunit-dom/pull/3) Configure Renovate ([@renovate[bot]](https://github.com/apps/renovate)) + +#### Committers: 2 + +- Patsy Issa ([@patsy-issa](https://github.com/patsy-issa)) +- Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) diff --git a/.vendor/eslint-plugin-qunit-dom/LICENSE b/.vendor/eslint-plugin-qunit-dom/LICENSE new file mode 100644 index 0000000000..8cdc7a06f1 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2020-2022 Mainmatter GmbH and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.vendor/eslint-plugin-qunit-dom/README.md b/.vendor/eslint-plugin-qunit-dom/README.md new file mode 100644 index 0000000000..6b0953acde --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/README.md @@ -0,0 +1,58 @@ +# eslint-plugin-qunit-dom + +An ESLint plugin for [qunit-dom] that automatically fixes the most common issues. + +[qunit-dom]: https://github.com/Mainmatter/qunit-dom + +## Compatibility + +- [ESLint](https://eslint.org/) 7.0.0 or above +- [Node.js](https://nodejs.org/) 12.x or above + +## Installation + +```shell +yarn add --dev eslint-plugin-qunit-dom +``` + +Or + +```shell +npm install --save-dev eslint-plugin-qunit-dom +``` + +## Usage + +Modify your `.eslintrc.js` by adding the `plugin:qunit-dom/recommended` config +to the `extends` list: + +```js +// .eslintrc.js +module.exports = { + extends: [ + // ... + 'plugin:qunit-dom/recommended', + ], +}; +``` + +## Rules + + + +💼 Configurations enabled in.\ +✅ Set in the `recommended` configuration.\ +🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). + +| Name | Description | 💼 | 🔧 | +| :-------------------------------------------------- | :---------------------------------------------------- | :-- | :-- | +| [no-checked-selector](rules/no-checked-selector.md) | disallow use of `assert.dom('.foo:checked').exists()` | ✅ | 🔧 | +| [no-ok-find](rules/no-ok-find.md) | disallow use of `assert.ok(find(...))` | ✅ | 🔧 | +| [require-assertion](rules/require-assertion.md) | require at least one assertion on `assert.dom()` | ✅ | 🔧 | + + + +## License + +This project is developed by and © [Mainmatter GmbH](http://mainmatter.com) +and contributors. It is released under the [MIT License](./LICENSE). diff --git a/.vendor/eslint-plugin-qunit-dom/configs.test.js b/.vendor/eslint-plugin-qunit-dom/configs.test.js new file mode 100644 index 0000000000..9ea4c514db --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/configs.test.js @@ -0,0 +1,19 @@ +describe('configs', () => { + it('recommended is stable', () => { + // if you change the list of recommended rules, make sure to release this + // as a breaking change!! + + expect(require('./index').configs.recommended).toMatchInlineSnapshot(` +Object { + "plugins": Array [ + "qunit-dom", + ], + "rules": Object { + "qunit-dom/no-checked-selector": "error", + "qunit-dom/no-ok-find": "error", + "qunit-dom/require-assertion": "error", + }, +} +`); + }); +}); diff --git a/.vendor/eslint-plugin-qunit-dom/index.js b/.vendor/eslint-plugin-qunit-dom/index.js new file mode 100644 index 0000000000..48015e3bca --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/index.js @@ -0,0 +1,43 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const rules = generateRulesMap(); + +module.exports = { + configs: { + recommended: generateRecommendedConfig(rules), + }, + rules, +}; + +function generateRulesMap() { + let rulesPath = path.join(__dirname, 'rules'); + let files = fs.readdirSync(rulesPath); + + let rulesMap = {}; + for (let file of files) { + if (file.endsWith('.js') && !file.endsWith('.test.js')) { + let ruleName = path.parse(file).name; + rulesMap[ruleName] = require(`./rules/${file}`); + } + } + return rulesMap; +} + +function generateRecommendedConfig(rules) { + let config = { + plugins: ['qunit-dom'], + rules: {}, + }; + + for (let ruleName of Object.keys(rules)) { + let rule = rules[ruleName]; + if (rule.meta.docs.recommended) { + config.rules[`qunit-dom/${ruleName}`] = 'error'; + } + } + + return config; +} diff --git a/.vendor/eslint-plugin-qunit-dom/package.json b/.vendor/eslint-plugin-qunit-dom/package.json new file mode 100644 index 0000000000..b7cbce7cf2 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/package.json @@ -0,0 +1,41 @@ +{ + "name": "eslint-plugin-qunit-dom", + "version": "0.2.0", + "license": "MIT", + "main": "index.js", + "repository": "https://github.com/Mainmatter/eslint-plugin-qunit-dom/", + "scripts": { + "lint": "npm-run-all \"lint:*\"", + "lint:js": "eslint .", + "lint:readme": "eslint-doc-generator --check", + "release": "release-it", + "test": "jest", + "update-readme": "eslint-doc-generator" + }, + "devDependencies": { + "eslint": "8.49.0", + "eslint-config-prettier": "9.0.0", + "eslint-doc-generator": "1.4.3", + "eslint-plugin-eslint-comments": "3.2.0", + "eslint-plugin-eslint-plugin": "5.1.1", + "eslint-plugin-filenames": "1.3.2", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-jest": "27.2.3", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.2.1", + "jest": "28.1.3", + "npm-run-all": "4.1.5", + "prettier": "2.8.8", + "release-it": "15.11.0", + "release-it-lerna-changelog": "5.0.0" + }, + "peerDependencies": { + "eslint": "^7.11.0 || ^8.0.0" + }, + "engines": { + "node": "12.* || 14.* || >= 16.*" + }, + "changelog": { + "repo": "Mainmatter/eslint-plugin-qunit-dom" + } +} diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.js b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.js new file mode 100644 index 0000000000..65732f97ba --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.js @@ -0,0 +1,186 @@ +const DOM_EXISTS_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.type="CallExpression"]' + + '[callee.object.callee.type="MemberExpression"]' + + '[callee.object.callee.object.name="assert"]' + + '[callee.object.callee.property.name="dom"]' + + '[callee.property.name=/^(exists|doesNotExist)$/]'; + +const OK_OR_NOTOK_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name=/^(ok|notOk)$/]' + + '[arguments.length>=1]'; + +const EQUAL_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name="equal"]' + + '[arguments.length>=2]' + + '[arguments.1.type="Literal"]'; + +const EQUAL_LENGTH_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name=/^(equal|strictEqual)$/]' + + '[arguments.length>=2]' + + '[arguments.0.type="MemberExpression"]' + + '[arguments.0.object.type="CallExpression"]' + + '[arguments.0.object.callee.type="Identifier"]' + + '[arguments.0.object.callee.name="find"]' + + '[arguments.0.property.type="Identifier"]' + + '[arguments.0.property.name="length"]' + + '[arguments.1.type="Literal"]'; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: "disallow use of `assert.dom('.foo:checked').exists()`", + recommended: true, + url: 'https://github.com/Mainmatter/eslint-plugin-qunit-dom/blob/main/rules/no-checked-selector.md', + }, + fixable: 'code', + schema: [], + messages: { + default: 'use assert.dom(...).isChecked()', + inverted: 'use assert.dom(...).isNotChecked()', + }, + }, + + create(context) { + let sourceCode = context.getSourceCode(); + + function fix(fixer, node, { inverted, target, rootElement, message }) { + let targetText = sourceCode.getText(target); + + let domArgs = targetText.substring(0, targetText.length - ':checked'.length - 1); + domArgs += targetText.substring(targetText.length - 1); + if (rootElement) { + domArgs += ', '; + domArgs += sourceCode.getText(rootElement); + } + + let assertion = inverted ? 'isNotChecked' : 'isChecked'; + + let messageText = message ? sourceCode.getText(message) : ''; + + return fixer.replaceText(node, `assert.dom(${domArgs}).${assertion}(${messageText})`); + } + + return { + [DOM_EXISTS_SELECTOR](node) { + let inverted = node.callee.property.name === 'doesNotExist'; + + let target = node.callee.object.arguments[0]; + if (!isValidFindArg(target)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let rootElement = node.callee.object.arguments[1]; + let message = node.arguments[0]; + return fix(fixer, node, { inverted, target, rootElement, message }); + }, + }); + }, + + [OK_OR_NOTOK_SELECTOR](node) { + let inverted = node.callee.property.name === 'notOk'; + + let firstArg = node.arguments[0]; + if (!isFindCall(firstArg) && !isIndexedFindCall(firstArg)) return; + + let findNode = firstArg.type === 'MemberExpression' ? firstArg.object : firstArg; + let target = findNode.arguments[0]; + if (!isValidFindArg(target)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let rootElement = findNode.arguments[1]; + let message = node.arguments[1]; + return fix(fixer, node, { inverted, target, rootElement, message }); + }, + }); + }, + + [EQUAL_SELECTOR](node) { + let secondArg = node.arguments[1]; + if (typeof secondArg.value !== 'boolean') return; + let inverted = !secondArg.value; + + let firstArg = node.arguments[0]; + if (!isFindCall(firstArg) && !isIndexedFindCall(firstArg)) return; + + let findNode = firstArg.type === 'MemberExpression' ? firstArg.object : firstArg; + let target = findNode.arguments[0]; + if (!isValidFindArg(target)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let rootElement = findNode.arguments[1]; + let message = node.arguments[2]; + return fix(fixer, node, { inverted, target, rootElement, message }); + }, + }); + }, + + [EQUAL_LENGTH_SELECTOR](node) { + let secondArg = node.arguments[1]; + let inverted = secondArg.value === 0; + + let findNode = node.arguments[0].object; + let target = findNode.arguments[0]; + if (!isValidFindArg(target)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let rootElement = findNode.arguments[1]; + let message = node.arguments[2]; + return fix(fixer, node, { inverted, target, rootElement, message }); + }, + }); + }, + }; + }, +}; + +// checks for `find(...)` +function isFindCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'find'; +} + +// checks for `find(...)[0]` +function isIndexedFindCall(node) { + return ( + node.type === 'MemberExpression' && + isFindCall(node.object) && + node.property.type === 'Literal' && + node.property.value === 0 + ); +} + +function isValidFindArg(node) { + return ( + node && + node.type === 'Literal' && + typeof node.value === 'string' && + node.value !== ':checked' && + node.value.endsWith(':checked') + ); +} diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.md b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.md new file mode 100644 index 0000000000..cf8eeda23f --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.md @@ -0,0 +1,24 @@ +# no-checked-selector + +💼 This rule is enabled in the ✅ `recommended` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +The `isChecked()` and `isNotChecked()` assertions should be preferred over +using the `:checked` CSS selector. + +## Examples + +This rule **forbids** the following: + +```js +assert.dom('.foo:checked').exists(); +``` + +This rule **allows** the following: + +```js +assert.dom('.foo').isChecked(); +``` diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.test.js b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.test.js new file mode 100644 index 0000000000..7bd2197cad --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-checked-selector.test.js @@ -0,0 +1,119 @@ +const { RuleTester } = require('eslint'); + +const rule = require('./no-checked-selector'); + +let ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +}); + +ruleTester.run('no-checked-selector', rule, { + valid: [ + 'assert()', + 'assert.foo', + "assert.foo('.foo:checked')", + "notAssert.dom('.foo:checked')", + "assert.dom('.foo:checked').somethingElse()", + "assert.dom('.foo:checked').exists", + "assert.dom(':checkedfoo').exists()", + "assert.dom(':checked').exists()", + 'assert.dom().exists()', + 'assert.dom(node).exists()', + 'assert.dom(42).exists()', + ], + invalid: [ + // assert.dom('.foo:checked').exists() + + { + code: "assert.dom('.foo:checked').exists();", + output: "assert.dom('.foo').isChecked();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.dom('.foo:checked').doesNotExist();", + output: "assert.dom('.foo').isNotChecked();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.dom('.foo:checked').exists('foo is checked');", + output: "assert.dom('.foo').isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.dom('.foo:checked', root).exists('foo is checked');", + output: "assert.dom('.foo', root).isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + + // assert.ok(find('.foo:checked')) + + { + code: "assert.ok(find('.foo:checked'));", + output: "assert.dom('.foo').isChecked();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.notOk(find('.foo:checked'));", + output: "assert.dom('.foo').isNotChecked();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.ok(find('.foo:checked'), 'foo is checked');", + output: "assert.dom('.foo').isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.notOk(find('.foo:checked'), 'foo is not checked');", + output: "assert.dom('.foo').isNotChecked('foo is not checked');", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.ok(find('.foo:checked', root), 'foo is checked');", + output: "assert.dom('.foo', root).isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.notOk(find('.foo:checked', root), 'foo is not checked');", + output: "assert.dom('.foo', root).isNotChecked('foo is not checked');", + errors: [{ messageId: 'inverted' }], + }, + + // assert.equal(find('.foo:checked'), true) + + { + code: "assert.equal(find('.foo:checked', root), true, 'foo is checked');", + output: "assert.dom('.foo', root).isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo:checked', root), false, 'foo is not checked');", + output: "assert.dom('.foo', root).isNotChecked('foo is not checked');", + errors: [{ messageId: 'inverted' }], + }, + + // assert.equal(find('.foo:checked').length, 1) + + { + code: "assert.equal(find('.foo:checked', root).length, 1, 'foo is checked');", + output: "assert.dom('.foo', root).isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.strictEqual(find('.foo:checked', root).length, 1, 'foo is checked');", + output: "assert.dom('.foo', root).isChecked('foo is checked');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo:checked', root).length, 0, 'foo is not checked');", + output: "assert.dom('.foo', root).isNotChecked('foo is not checked');", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.strictEqual(find('.foo:checked', root).length, 0, 'foo is not checked');", + output: "assert.dom('.foo', root).isNotChecked('foo is not checked');", + errors: [{ messageId: 'inverted' }], + }, + ], +}); diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.js b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.js new file mode 100644 index 0000000000..8f60b153ee --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.js @@ -0,0 +1,186 @@ +const OK_OR_NOTOK_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name=/^(ok|notOk)$/]' + + '[arguments.length>=1]'; + +const EQUAL_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name="equal"]' + + '[arguments.length>=2]' + + '[arguments.1.type="Literal"]'; + +const EQUAL_LENGTH_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.name="assert"]' + + '[callee.property.name=/^(equal|strictEqual)$/]' + + '[arguments.length>=2]' + + '[arguments.0.type="MemberExpression"]' + + '[arguments.0.object.type="CallExpression"]' + + '[arguments.0.object.callee.type="Identifier"]' + + '[arguments.0.object.callee.name="find"]' + + '[arguments.0.property.type="Identifier"]' + + '[arguments.0.property.name="length"]' + + '[arguments.1.type="Literal"]'; + +// see https://api.jquery.com/category/selectors/jquery-selector-extensions/ +const JQUERY_SELECTOR_EXTENSIONS = [ + ':animated', + ':button', + ':checkbox', + ':contains(', + ':eq(', + ':even', + ':file', + ':first', + ':gt(', + ':has(', + ':header', + ':hidden', + ':image', + ':input', + ':last', + ':lt(', + ':odd', + ':parent', + ':password', + ':radio', + ':reset', + ':selected', + ':submit', + ':text', + ':visible', +]; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow use of `assert.ok(find(...))`', + recommended: true, + url: 'https://github.com/Mainmatter/eslint-plugin-qunit-dom/blob/main/rules/no-ok-find.md', + }, + fixable: 'code', + schema: [], + messages: { + default: 'use assert.dom(...).exists()', + inverted: 'use assert.dom(...).doesNotExists()', + }, + }, + + create(context) { + let sourceCode = context.getSourceCode(); + + function fix(fixer, node, { inverted, findNode, messageNode }) { + let domArgs = sourceCode.getText(findNode.arguments[0]); + let scopeArg = findNode.arguments[1]; + if (scopeArg) { + domArgs += ', '; + domArgs += sourceCode.getText(scopeArg); + } + + let assertion = inverted ? 'doesNotExist' : 'exists'; + + let messageArgText = messageNode ? sourceCode.getText(messageNode) : ''; + + return fixer.replaceText(node, `assert.dom(${domArgs}).${assertion}(${messageArgText})`); + } + + return { + [OK_OR_NOTOK_SELECTOR](node) { + let inverted = node.callee.property.name === 'notOk'; + + let firstArg = node.arguments[0]; + if (!isFindCall(firstArg) && !isIndexedFindCall(firstArg)) return; + + let findNode = firstArg.type === 'MemberExpression' ? firstArg.object : firstArg; + let firstFindArg = findNode.arguments[0]; + if (!isValidFindArg(firstFindArg)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let messageNode = node.arguments[1]; + return fix(fixer, node, { inverted, findNode, messageNode }); + }, + }); + }, + + [EQUAL_SELECTOR](node) { + let secondArg = node.arguments[1]; + if (typeof secondArg.value !== 'boolean') return; + let inverted = !secondArg.value; + + let firstArg = node.arguments[0]; + if (!isFindCall(firstArg) && !isIndexedFindCall(firstArg)) return; + + let findNode = firstArg.type === 'MemberExpression' ? firstArg.object : firstArg; + let findArgs = findNode.arguments; + let firstFindArg = findArgs[0]; + if (!isValidFindArg(firstFindArg)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let messageNode = node.arguments[2]; + return fix(fixer, node, { inverted, findNode, messageNode }); + }, + }); + }, + + [EQUAL_LENGTH_SELECTOR](node) { + let secondArg = node.arguments[1]; + let inverted = secondArg.value === 0; + + let findNode = node.arguments[0].object; + let firstFindArg = findNode.arguments[0]; + if (!isValidFindArg(firstFindArg)) return; + + context.report({ + node: node, + messageId: inverted ? 'inverted' : 'default', + + fix(fixer) { + let messageNode = node.arguments[2]; + return fix(fixer, node, { inverted, findNode, messageNode }); + }, + }); + }, + }; + }, +}; + +// checks for `find(...)` +function isFindCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'find'; +} + +// checks for `find(...)[0]` +function isIndexedFindCall(node) { + return ( + node.type === 'MemberExpression' && + isFindCall(node.object) && + node.property.type === 'Literal' && + node.property.value === 0 + ); +} + +function isValidFindArg(node) { + if (!node) return false; + if (node.type === 'Literal') { + return typeof node.value === 'string' && !hasJQuerySelector(node.value); + } + return true; +} + +function hasJQuerySelector(selector) { + return JQUERY_SELECTOR_EXTENSIONS.some(it => selector.includes(it)); +} diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.md b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.md new file mode 100644 index 0000000000..39f1f5c0cc --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.md @@ -0,0 +1,32 @@ +# no-ok-find + +💼 This rule is enabled in the ✅ `recommended` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +`assert.ok/notOk(find('.foo'))` can be replaced with +`assert.dom('.foo').exists/doesNotExist()`. + +## Examples + +This rule **forbids** the following: + +```js +assert.ok(find('.foo')); +``` + +```js +assert.notOk(find('.foo')); +``` + +This rule **allows** the following: + +```js +assert.dom('.foo').exists(); +``` + +```js +assert.dom('.foo').doesNotExist(); +``` diff --git a/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.test.js b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.test.js new file mode 100644 index 0000000000..85205523c4 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/no-ok-find.test.js @@ -0,0 +1,244 @@ +const { RuleTester } = require('eslint'); + +const rule = require('./no-ok-find'); + +let ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +}); + +ruleTester.run('no-ok-find', rule, { + valid: [ + "notAssert.ok(find('.foo'));", + "assert.foo(find('.bar'));", + 'assert.ok;', + 'assert.ok();', + 'assert.ok(1);', + "assert.ok(notFind('.foo'));", + 'assert.ok(find());', + "assert.token(find('.foo'));", + + "notAssert.notOk(find('.foo'));", + 'assert.notOk;', + 'assert.notOk();', + 'assert.notOk(1);', + "assert.notOk(notFind('.foo'));", + 'assert.notOk(find());', + + // from https://github.com/Mainmatter/qunit-dom-codemod/blob/master/__testfixtures__/qunit-dom-codemod/ok-find.input.js + "assert.ok(find('input:first'));", + "assert.ok(find('input:contains(foo)'));", + "assert.equal(find('.foo'));", + "assert.strictEqual(find('.foo'));", + 'assert.ok(true);', + 'assert.equal(foo(), true);', + 'assert.strictEqual(foo(), true);', + + // from https://github.com/Mainmatter/qunit-dom-codemod/blob/master/__testfixtures__/qunit-dom-codemod/ok-find.input.js + "assert.notOk(find('input:first'));", + "assert.notOk(find('input:contains(foo)'));", + 'assert.notOk(true);', + + "assert.equal(find('.foo'), 'foo');", + 'assert.equal(find(), true);', + 'assert.equal(find(42), true);', + + "assert.strictEqual(find('.foo'), true);", + "assert.strictEqual(find('.foo')[0], true);", + "assert.strictEqual(find('.foo'), true, 'custom message');", + "assert.strictEqual(find('.foo')[0], true, 'custom message');", + "assert.strictEqual(find('.foo', root), true);", + "assert.strictEqual(find('.foo', root)[0], true);", + + "assert.strictEqual(find('.foo'), false);", + "assert.strictEqual(find('.foo')[0], false);", + "assert.strictEqual(find('.foo'), false, 'custom message');", + "assert.strictEqual(find('.foo')[0], false, 'custom message');", + "assert.strictEqual(find('.foo', root), false);", + "assert.strictEqual(find('.foo', root)[0], false);", + + 'assert.equal(find(42).length, 0);', + ], + + invalid: [ + // from https://github.com/Mainmatter/qunit-dom-codemod/blob/master/__testfixtures__/qunit-dom-codemod/ok-find.input.js + + { + code: "assert.ok(find('.foo'));", + output: "assert.dom('.foo').exists();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.ok(find('.foo')[0]);", + output: "assert.dom('.foo').exists();", + errors: [{ messageId: 'default' }], + }, + { + code: 'assert.ok(find(foo));', + output: 'assert.dom(foo).exists();', + errors: [{ messageId: 'default' }], + }, + { + code: 'assert.ok(find(foo.bar));', + output: 'assert.dom(foo.bar).exists();', + errors: [{ messageId: 'default' }], + }, + + { + code: "assert.ok(find('.foo'), 'custom message');", + output: "assert.dom('.foo').exists('custom message');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.ok(find('.foo')[0], 'custom message');", + output: "assert.dom('.foo').exists('custom message');", + errors: [{ messageId: 'default' }], + }, + + { + code: "assert.ok(find('.foo', root));", + output: "assert.dom('.foo', root).exists();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.ok(find('.foo', root)[0]);", + output: "assert.dom('.foo', root).exists();", + errors: [{ messageId: 'default' }], + }, + + { + code: "assert.equal(find('.foo'), true);", + output: "assert.dom('.foo').exists();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo')[0], true);", + output: "assert.dom('.foo').exists();", + errors: [{ messageId: 'default' }], + }, + + { + code: "assert.equal(find('.foo'), true, 'custom message');", + output: "assert.dom('.foo').exists('custom message');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo')[0], true, 'custom message');", + output: "assert.dom('.foo').exists('custom message');", + errors: [{ messageId: 'default' }], + }, + + { + code: "assert.equal(find('.foo', root), true);", + output: "assert.dom('.foo', root).exists();", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo', root)[0], true);", + output: "assert.dom('.foo', root).exists();", + errors: [{ messageId: 'default' }], + }, + + // from https://github.com/Mainmatter/qunit-dom-codemod/blob/master/__testfixtures__/qunit-dom-codemod/not-ok-find.input.js + + { + code: "assert.notOk(find('.foo'));", + output: "assert.dom('.foo').doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.notOk(find('.foo')[0]);", + output: "assert.dom('.foo').doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + { + code: 'assert.notOk(find(foo));', + output: 'assert.dom(foo).doesNotExist();', + errors: [{ messageId: 'inverted' }], + }, + { + code: 'assert.notOk(find(foo.bar));', + output: 'assert.dom(foo.bar).doesNotExist();', + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.notOk(find('.foo'), 'custom message');", + output: "assert.dom('.foo').doesNotExist('custom message');", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.notOk(find('.foo')[0], 'custom message');", + output: "assert.dom('.foo').doesNotExist('custom message');", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.notOk(find('.foo', root));", + output: "assert.dom('.foo', root).doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.notOk(find('.foo', root)[0]);", + output: "assert.dom('.foo', root).doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.equal(find('.foo'), false);", + output: "assert.dom('.foo').doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.equal(find('.foo')[0], false);", + output: "assert.dom('.foo').doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.equal(find('.foo'), false, 'custom message');", + output: "assert.dom('.foo').doesNotExist('custom message');", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.equal(find('.foo')[0], false, 'custom message');", + output: "assert.dom('.foo').doesNotExist('custom message');", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.equal(find('.foo', root), false);", + output: "assert.dom('.foo', root).doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + { + code: "assert.equal(find('.foo', root)[0], false);", + output: "assert.dom('.foo', root).doesNotExist();", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.equal(find('.foo', root).length, 1, 'foo exists');", + output: "assert.dom('.foo', root).exists('foo exists');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.equal(find('.foo', root).length, 0, 'foo does not exist');", + output: "assert.dom('.foo', root).doesNotExist('foo does not exist');", + errors: [{ messageId: 'inverted' }], + }, + + { + code: "assert.strictEqual(find('.foo', root).length, 1, 'foo exists');", + output: "assert.dom('.foo', root).exists('foo exists');", + errors: [{ messageId: 'default' }], + }, + { + code: "assert.strictEqual(find('.foo', root).length, 0, 'foo does not exist');", + output: "assert.dom('.foo', root).doesNotExist('foo does not exist');", + errors: [{ messageId: 'inverted' }], + }, + ], +}); diff --git a/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.js b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.js new file mode 100644 index 0000000000..45fc0954d5 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.js @@ -0,0 +1,41 @@ +const ASSERT_DOM_SELECTOR = + 'CallExpression' + + '[callee.type="MemberExpression"]' + + '[callee.object.type="Identifier"]' + + '[callee.object.name="assert"]' + + '[callee.property.type="Identifier"]' + + '[callee.property.name="dom"]'; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'require at least one assertion on `assert.dom()`', + recommended: true, + url: 'https://github.com/Mainmatter/eslint-plugin-qunit-dom/blob/main/rules/require-assertion.md', + }, + fixable: 'code', + schema: [], + messages: { + default: 'use at least one assertion on assert.dom(...)', + }, + }, + + create(context) { + return { + [ASSERT_DOM_SELECTOR](node) { + if (node.parent.type === 'ExpressionStatement') { + context.report({ + node: node, + messageId: 'default', + fix(fixer) { + return fixer.insertTextAfter(node, '.exists()'); + }, + }); + } else { + return; + } + }, + }; + }, +}; diff --git a/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.md b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.md new file mode 100644 index 0000000000..3ba1ecb1b7 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.md @@ -0,0 +1,23 @@ +# require-assertion + +💼 This rule is enabled in the ✅ `recommended` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +At least one assertion should be made after calling `assert.dom()`. + +## Examples + +This rule **forbids** the following: + +```js +assert.dom('.foo'); +``` + +This rule **allows** the following: + +```js +assert.dom('.foo').exists(); +``` diff --git a/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.test.js b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.test.js new file mode 100644 index 0000000000..aeea194278 --- /dev/null +++ b/.vendor/eslint-plugin-qunit-dom/rules/require-assertion.test.js @@ -0,0 +1,27 @@ +const { RuleTester } = require('eslint'); + +const rule = require('./require-assertion'); + +let ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +}); + +ruleTester.run('require-assertion', rule, { + valid: ['assert.dom().exists()', 'assert.dom(node).exists()'], + + invalid: [ + { + code: 'assert.dom()', + output: 'assert.dom().exists()', + errors: [{ messageId: 'default' }], + }, + { + code: 'assert.dom(node)', + output: 'assert.dom(node).exists()', + errors: [{ messageId: 'default' }], + }, + ], +}); diff --git a/packages/host/package.json b/packages/host/package.json index c0523f47dc..e0354f2638 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -148,7 +148,7 @@ "eslint-plugin-n": "catalog:", "eslint-plugin-prettier": "catalog:", "eslint-plugin-qunit": "catalog:", - "eslint-plugin-qunit-dom": "catalog:", + "eslint-plugin-qunit-dom": "file:../../.vendor/eslint-plugin-qunit-dom", "ethers": "catalog:", "eventemitter3": "catalog:", "fast-json-stable-stringify": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1406acc92..c18b678989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -423,9 +423,6 @@ catalogs: eslint-plugin-qunit: specifier: ^8.0.1 version: 8.2.6 - eslint-plugin-qunit-dom: - specifier: mainmatter/eslint-plugin-qunit-dom#d66c84190b018deb96c715c7f2bde3536a9c704b - version: 0.2.0 eslint-plugin-simple-import-sort: specifier: ^8.0.0 version: 8.0.0 @@ -2309,8 +2306,8 @@ importers: specifier: 'catalog:' version: 8.2.6(eslint@8.57.1) eslint-plugin-qunit-dom: - specifier: 'catalog:' - version: https://codeload.github.com/mainmatter/eslint-plugin-qunit-dom/tar.gz/d66c84190b018deb96c715c7f2bde3536a9c704b(eslint@8.57.1) + specifier: file:../../.vendor/eslint-plugin-qunit-dom + version: file:.vendor/eslint-plugin-qunit-dom(eslint@8.57.1) ethers: specifier: 'catalog:' version: 6.16.0 @@ -10148,9 +10145,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-qunit-dom@https://codeload.github.com/mainmatter/eslint-plugin-qunit-dom/tar.gz/d66c84190b018deb96c715c7f2bde3536a9c704b: - resolution: {gitHosted: true, tarball: https://codeload.github.com/mainmatter/eslint-plugin-qunit-dom/tar.gz/d66c84190b018deb96c715c7f2bde3536a9c704b} - version: 0.2.0 + eslint-plugin-qunit-dom@file:.vendor/eslint-plugin-qunit-dom: + resolution: {directory: .vendor/eslint-plugin-qunit-dom, type: directory} engines: {node: 12.* || 14.* || >= 16.*} peerDependencies: eslint: ^7.11.0 || ^8.0.0 @@ -24579,7 +24575,7 @@ snapshots: '@types/eslint': 8.56.5 eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-plugin-qunit-dom@https://codeload.github.com/mainmatter/eslint-plugin-qunit-dom/tar.gz/d66c84190b018deb96c715c7f2bde3536a9c704b(eslint@8.57.1): + eslint-plugin-qunit-dom@file:.vendor/eslint-plugin-qunit-dom(eslint@8.57.1): dependencies: eslint: 8.57.1