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-setup.sh b/.devcontainer/claude-web-setup.sh new file mode 100755 index 0000000000..ff2cb4beba --- /dev/null +++ b/.devcontainer/claude-web-setup.sh @@ -0,0 +1,99 @@ +#!/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 + 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: +# +# .devcontainer/claude-web-start.sh +# +# 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. 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. +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 + +# 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 +# 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 + +# 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. +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 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..2cc4cfa7fd --- /dev/null +++ b/.devcontainer/claude-web-start.sh @@ -0,0 +1,79 @@ +#!/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 + +# 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 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/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. 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