From 548cadacff83e143bf2f7309045526369cadbaa5 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Sun, 29 Mar 2026 21:40:24 +0800 Subject: [PATCH] test: init e2e (#34193) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/main-ci.yml | 70 + .github/workflows/web-e2e.yml | 72 + docker/docker-compose.middleware.yaml | 2 + e2e/.gitignore | 6 + e2e/AGENTS.md | 164 + e2e/README.md | 3 + e2e/cucumber.config.ts | 19 + e2e/features/apps/create-app.feature | 10 + .../smoke/authenticated-entry.feature | 8 + e2e/features/smoke/install.feature | 7 + .../step-definitions/apps/create-app.steps.ts | 29 + .../step-definitions/common/auth.steps.ts | 11 + .../common/navigation.steps.ts | 23 + .../step-definitions/smoke/install.steps.ts | 12 + e2e/features/support/hooks.ts | 90 + e2e/features/support/world.ts | 68 + e2e/fixtures/auth.ts | 148 + e2e/package.json | 34 + e2e/pnpm-lock.yaml | 2632 +++++++++++++++++ e2e/scripts/common.ts | 242 ++ e2e/scripts/run-cucumber.ts | 154 + e2e/scripts/setup.ts | 306 ++ e2e/support/process.ts | 178 ++ e2e/support/web-server.ts | 83 + e2e/test-env.ts | 12 + e2e/tsconfig.json | 25 + e2e/vite.config.ts | 15 + 27 files changed, 4423 insertions(+) create mode 100644 .github/workflows/web-e2e.yml create mode 100644 e2e/.gitignore create mode 100644 e2e/AGENTS.md create mode 100644 e2e/README.md create mode 100644 e2e/cucumber.config.ts create mode 100644 e2e/features/apps/create-app.feature create mode 100644 e2e/features/smoke/authenticated-entry.feature create mode 100644 e2e/features/smoke/install.feature create mode 100644 e2e/features/step-definitions/apps/create-app.steps.ts create mode 100644 e2e/features/step-definitions/common/auth.steps.ts create mode 100644 e2e/features/step-definitions/common/navigation.steps.ts create mode 100644 e2e/features/step-definitions/smoke/install.steps.ts create mode 100644 e2e/features/support/hooks.ts create mode 100644 e2e/features/support/world.ts create mode 100644 e2e/fixtures/auth.ts create mode 100644 e2e/package.json create mode 100644 e2e/pnpm-lock.yaml create mode 100644 e2e/scripts/common.ts create mode 100644 e2e/scripts/run-cucumber.ts create mode 100644 e2e/scripts/setup.ts create mode 100644 e2e/support/process.ts create mode 100644 e2e/support/web-server.ts create mode 100644 e2e/test-env.ts create mode 100644 e2e/tsconfig.json create mode 100644 e2e/vite.config.ts diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 6fffbefce01..71401caa5de 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -42,6 +42,7 @@ jobs: runs-on: ubuntu-latest outputs: api-changed: ${{ steps.changes.outputs.api }} + e2e-changed: ${{ steps.changes.outputs.e2e }} web-changed: ${{ steps.changes.outputs.web }} vdb-changed: ${{ steps.changes.outputs.vdb }} migration-changed: ${{ steps.changes.outputs.migration }} @@ -59,6 +60,16 @@ jobs: - 'web/**' - '.github/workflows/web-tests.yml' - '.github/actions/setup-web/**' + e2e: + - 'api/**' + - 'api/pyproject.toml' + - 'api/uv.lock' + - 'e2e/**' + - 'web/**' + - 'docker/docker-compose.middleware.yaml' + - 'docker/middleware.env.example' + - '.github/workflows/web-e2e.yml' + - '.github/actions/setup-web/**' vdb: - 'api/core/rag/datasource/**' - 'docker/**' @@ -190,6 +201,65 @@ jobs: echo "Web tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2 exit 1 + web-e2e-run: + name: Run Web Full-Stack E2E + needs: + - pre_job + - check-changes + if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed == 'true' + uses: ./.github/workflows/web-e2e.yml + + web-e2e-skip: + name: Skip Web Full-Stack E2E + needs: + - pre_job + - check-changes + if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true' + runs-on: ubuntu-latest + steps: + - name: Report skipped web full-stack e2e + run: echo "No E2E-related changes detected; skipping web full-stack E2E." + + web-e2e: + name: Web Full-Stack E2E + if: ${{ always() }} + needs: + - pre_job + - check-changes + - web-e2e-run + - web-e2e-skip + runs-on: ubuntu-latest + steps: + - name: Finalize Web Full-Stack E2E status + env: + SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }} + TESTS_CHANGED: ${{ needs.check-changes.outputs.e2e-changed }} + RUN_RESULT: ${{ needs.web-e2e-run.result }} + SKIP_RESULT: ${{ needs.web-e2e-skip.result }} + run: | + if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then + echo "Web full-stack E2E was skipped because this workflow run duplicated a successful or newer run." + exit 0 + fi + + if [[ "$TESTS_CHANGED" == 'true' ]]; then + if [[ "$RUN_RESULT" == 'success' ]]; then + echo "Web full-stack E2E ran successfully." + exit 0 + fi + + echo "Web full-stack E2E was required but finished with result: $RUN_RESULT" >&2 + exit 1 + fi + + if [[ "$SKIP_RESULT" == 'success' ]]; then + echo "Web full-stack E2E was skipped because no E2E-related files changed." + exit 0 + fi + + echo "Web full-stack E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2 + exit 1 + style-check: name: Style Check needs: pre_job diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml new file mode 100644 index 00000000000..8035d1ef8ed --- /dev/null +++ b/.github/workflows/web-e2e.yml @@ -0,0 +1,72 @@ +name: Web Full-Stack E2E + +on: + workflow_call: + +permissions: + contents: read + +concurrency: + group: web-e2e-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + name: Web Full-Stack E2E + runs-on: ubuntu-latest + defaults: + run: + shell: bash + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup web dependencies + uses: ./.github/actions/setup-web + + - name: Install E2E package dependencies + working-directory: ./e2e + run: vp install --frozen-lockfile + + - name: Setup UV and Python + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + enable-cache: true + python-version: "3.12" + cache-dependency-glob: api/uv.lock + + - name: Install API dependencies + run: uv sync --project api --dev + + - name: Install Playwright browser + working-directory: ./e2e + run: vp run e2e:install + + - name: Run isolated source-api and built-web Cucumber E2E tests + working-directory: ./e2e + env: + E2E_ADMIN_EMAIL: e2e-admin@example.com + E2E_ADMIN_NAME: E2E Admin + E2E_ADMIN_PASSWORD: E2eAdmin12345 + E2E_FORCE_WEB_BUILD: "1" + E2E_INIT_PASSWORD: E2eInit12345 + run: vp run e2e:full + + - name: Upload Cucumber report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: cucumber-report + path: e2e/cucumber-report + retention-days: 7 + + - name: Upload E2E logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: e2e-logs + path: e2e/.logs + retention-days: 7 diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 1746bb567a7..911da70a737 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -127,6 +127,8 @@ services: restart: always env_file: - ./middleware.env + extra_hosts: + - "host.docker.internal:host-gateway" environment: # Use the shared environment variables. LOG_OUTPUT_FORMAT: ${LOG_OUTPUT_FORMAT:-text} diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000000..96c1e0f3a18 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.auth/ +playwright-report/ +test-results/ +cucumber-report/ +.logs/ diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md new file mode 100644 index 00000000000..245c9863d46 --- /dev/null +++ b/e2e/AGENTS.md @@ -0,0 +1,164 @@ +# E2E + +This package contains the repository-level end-to-end tests for Dify. + +This file is the canonical package guide for `e2e/`. Keep detailed workflow, architecture, debugging, and reporting documentation here. Keep `README.md` as a minimal pointer to this file so the two documents do not drift. + +The suite uses Cucumber for scenario definitions and Playwright as the browser execution layer. + +It tests: + +- backend API started from source +- frontend served from the production artifact +- middleware services started from Docker + +## Prerequisites + +- Node.js `^22.22.1` +- `pnpm` +- `uv` +- Docker + +Install Playwright browsers once: + +```bash +cd e2e +pnpm install +pnpm e2e:install +pnpm check +``` + +Use `pnpm check` as the default local verification step after editing E2E TypeScript, Cucumber support code, or feature glue. It runs formatting, linting, and type checks for this package. + +Common commands: + +```bash +# authenticated-only regression (default excludes @fresh) +# expects backend API, frontend artifact, and middleware stack to already be running +pnpm e2e + +# full reset + fresh install + authenticated scenarios +# starts required middleware/dependencies for you +pnpm e2e:full + +# run a tagged subset +pnpm e2e -- --tags @smoke + +# headed browser +pnpm e2e:headed -- --tags @smoke + +# slow down browser actions for local debugging +E2E_SLOW_MO=500 pnpm e2e:headed -- --tags @smoke +``` + +Frontend artifact behavior: + +- if `web/.next/BUILD_ID` exists, E2E reuses the existing build by default +- if you set `E2E_FORCE_WEB_BUILD=1`, E2E rebuilds the frontend before starting it + +## Lifecycle + +```mermaid +flowchart TD + A["Start E2E run"] --> B["run-cucumber.ts orchestrates setup/API/frontend"] + B --> C["support/web-server.ts starts or reuses frontend directly"] + C --> D["Cucumber loads config, steps, and support modules"] + D --> E["BeforeAll bootstraps shared auth state via /install"] + E --> F{"Which command is running?"} + F -->|`pnpm e2e`| G["Run config default tags: not @fresh and not @skip"] + F -->|`pnpm e2e:full*`| H["Override tags to not @skip"] + G --> I["Per-scenario BrowserContext from shared browser"] + H --> I + I --> J["Failure artifacts written to cucumber-report/artifacts"] +``` + +Ownership is split like this: + +- `scripts/setup.ts` is the single environment entrypoint for reset, middleware, backend, and frontend startup +- `run-cucumber.ts` orchestrates the E2E run and Cucumber invocation +- `support/web-server.ts` manages frontend reuse, startup, readiness, and shutdown +- `features/support/hooks.ts` manages auth bootstrap, scenario lifecycle, and diagnostics +- `features/support/world.ts` owns per-scenario typed context +- `features/step-definitions/` holds domain-oriented glue so the official VS Code Cucumber plugin works with default conventions when `e2e/` is opened as the workspace root + +Package layout: + +- `features/`: Gherkin scenarios grouped by capability +- `features/step-definitions/`: domain-oriented step definitions +- `features/support/hooks.ts`: suite lifecycle, auth-state bootstrap, diagnostics +- `features/support/world.ts`: shared scenario context +- `support/web-server.ts`: typed frontend startup/reuse logic +- `scripts/setup.ts`: reset and service lifecycle commands +- `scripts/run-cucumber.ts`: Cucumber orchestration entrypoint + +Behavior depends on instance state: + +- uninitialized instance: completes install and stores authenticated state +- initialized instance: signs in and reuses authenticated state + +Because of that, the `@fresh` install scenario only runs in the `pnpm e2e:full*` flows. The default `pnpm e2e*` flows exclude `@fresh` via Cucumber config tags so they can be re-run against an already initialized instance. + +Reset all persisted E2E state: + +```bash +pnpm e2e:reset +``` + +This removes: + +- `docker/volumes/db/data` +- `docker/volumes/redis/data` +- `docker/volumes/weaviate` +- `docker/volumes/plugin_daemon` +- `e2e/.auth` +- `e2e/.logs` +- `e2e/cucumber-report` + +Start the full middleware stack: + +```bash +pnpm e2e:middleware:up +``` + +Stop the full middleware stack: + +```bash +pnpm e2e:middleware:down +``` + +The middleware stack includes: + +- PostgreSQL +- Redis +- Weaviate +- Sandbox +- SSRF proxy +- Plugin daemon + +Fresh install verification: + +```bash +pnpm e2e:full +``` + +Run the Cucumber suite against an already running middleware stack: + +```bash +pnpm e2e:middleware:up +pnpm e2e +pnpm e2e:middleware:down +``` + +Artifacts and diagnostics: + +- `cucumber-report/report.html`: HTML report +- `cucumber-report/report.json`: JSON report +- `cucumber-report/artifacts/`: failure screenshots and HTML captures +- `.logs/cucumber-api.log`: backend startup log +- `.logs/cucumber-web.log`: frontend startup log + +Open the HTML report locally with: + +```bash +open cucumber-report/report.html +``` diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000000..9b4046eaff7 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,3 @@ +# E2E + +Canonical documentation for this package lives in [AGENTS.md](./AGENTS.md). diff --git a/e2e/cucumber.config.ts b/e2e/cucumber.config.ts new file mode 100644 index 00000000000..c162a6562e9 --- /dev/null +++ b/e2e/cucumber.config.ts @@ -0,0 +1,19 @@ +import type { IConfiguration } from '@cucumber/cucumber' + +const config = { + format: [ + 'progress-bar', + 'summary', + 'html:./cucumber-report/report.html', + 'json:./cucumber-report/report.json', + ], + import: ['features/**/*.ts'], + parallel: 1, + paths: ['features/**/*.feature'], + tags: process.env.E2E_CUCUMBER_TAGS || 'not @fresh and not @skip', + timeout: 60_000, +} satisfies Partial & { + timeout: number +} + +export default config diff --git a/e2e/features/apps/create-app.feature b/e2e/features/apps/create-app.feature new file mode 100644 index 00000000000..c0ca8ea4e00 --- /dev/null +++ b/e2e/features/apps/create-app.feature @@ -0,0 +1,10 @@ +@apps @authenticated +Feature: Create app + Scenario: Create a new blank app and redirect to the editor + Given I am signed in as the default E2E admin + When I open the apps console + And I start creating a blank app + And I enter a unique E2E app name + And I confirm app creation + Then I should land on the app editor + And I should see the "Orchestrate" text diff --git a/e2e/features/smoke/authenticated-entry.feature b/e2e/features/smoke/authenticated-entry.feature new file mode 100644 index 00000000000..3c1191a330d --- /dev/null +++ b/e2e/features/smoke/authenticated-entry.feature @@ -0,0 +1,8 @@ +@smoke @authenticated +Feature: Authenticated app console + Scenario: Open the apps console with the shared authenticated state + Given I am signed in as the default E2E admin + When I open the apps console + Then I should stay on the apps console + And I should see the "Create from Blank" button + And I should not see the "Sign in" button diff --git a/e2e/features/smoke/install.feature b/e2e/features/smoke/install.feature new file mode 100644 index 00000000000..39fc1f996b9 --- /dev/null +++ b/e2e/features/smoke/install.feature @@ -0,0 +1,7 @@ +@smoke @fresh +Feature: Fresh installation bootstrap + Scenario: Complete the initial installation bootstrap on a fresh instance + Given the last authentication bootstrap came from a fresh install + When I open the apps console + Then I should stay on the apps console + And I should see the "Create from Blank" button diff --git a/e2e/features/step-definitions/apps/create-app.steps.ts b/e2e/features/step-definitions/apps/create-app.steps.ts new file mode 100644 index 00000000000..b8e76c6f064 --- /dev/null +++ b/e2e/features/step-definitions/apps/create-app.steps.ts @@ -0,0 +1,29 @@ +import { Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import type { DifyWorld } from '../../support/world' + +When('I start creating a blank app', async function (this: DifyWorld) { + const page = this.getPage() + + await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible() + await page.getByRole('button', { name: 'Create from Blank' }).click() +}) + +When('I enter a unique E2E app name', async function (this: DifyWorld) { + const appName = `E2E App ${Date.now()}` + + await this.getPage().getByPlaceholder('Give your app a name').fill(appName) +}) + +When('I confirm app creation', async function (this: DifyWorld) { + const createButton = this.getPage() + .getByRole('button', { name: /^Create(?:\s|$)/ }) + .last() + + await expect(createButton).toBeEnabled() + await createButton.click() +}) + +Then('I should land on the app editor', async function (this: DifyWorld) { + await expect(this.getPage()).toHaveURL(/\/app\/[^/]+\/(workflow|configuration)(?:\?.*)?$/) +}) diff --git a/e2e/features/step-definitions/common/auth.steps.ts b/e2e/features/step-definitions/common/auth.steps.ts new file mode 100644 index 00000000000..bf03c2d8f43 --- /dev/null +++ b/e2e/features/step-definitions/common/auth.steps.ts @@ -0,0 +1,11 @@ +import { Given } from '@cucumber/cucumber' +import type { DifyWorld } from '../../support/world' + +Given('I am signed in as the default E2E admin', async function (this: DifyWorld) { + const session = await this.getAuthSession() + + this.attach( + `Authenticated as ${session.adminEmail} using ${session.mode} flow at ${session.baseURL}.`, + 'text/plain', + ) +}) diff --git a/e2e/features/step-definitions/common/navigation.steps.ts b/e2e/features/step-definitions/common/navigation.steps.ts new file mode 100644 index 00000000000..b18ff035fa6 --- /dev/null +++ b/e2e/features/step-definitions/common/navigation.steps.ts @@ -0,0 +1,23 @@ +import { Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import type { DifyWorld } from '../../support/world' + +When('I open the apps console', async function (this: DifyWorld) { + await this.getPage().goto('/apps') +}) + +Then('I should stay on the apps console', async function (this: DifyWorld) { + await expect(this.getPage()).toHaveURL(/\/apps(?:\?.*)?$/) +}) + +Then('I should see the {string} button', async function (this: DifyWorld, label: string) { + await expect(this.getPage().getByRole('button', { name: label })).toBeVisible() +}) + +Then('I should not see the {string} button', async function (this: DifyWorld, label: string) { + await expect(this.getPage().getByRole('button', { name: label })).not.toBeVisible() +}) + +Then('I should see the {string} text', async function (this: DifyWorld, text: string) { + await expect(this.getPage().getByText(text)).toBeVisible({ timeout: 30_000 }) +}) diff --git a/e2e/features/step-definitions/smoke/install.steps.ts b/e2e/features/step-definitions/smoke/install.steps.ts new file mode 100644 index 00000000000..857e01a9717 --- /dev/null +++ b/e2e/features/step-definitions/smoke/install.steps.ts @@ -0,0 +1,12 @@ +import { Given } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import type { DifyWorld } from '../../support/world' + +Given( + 'the last authentication bootstrap came from a fresh install', + async function (this: DifyWorld) { + const session = await this.getAuthSession() + + expect(session.mode).toBe('install') + }, +) diff --git a/e2e/features/support/hooks.ts b/e2e/features/support/hooks.ts new file mode 100644 index 00000000000..a6862d79f54 --- /dev/null +++ b/e2e/features/support/hooks.ts @@ -0,0 +1,90 @@ +import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber' +import { chromium, type Browser } from '@playwright/test' +import { mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { ensureAuthenticatedState } from '../../fixtures/auth' +import { baseURL, cucumberHeadless, cucumberSlowMo } from '../../test-env' +import type { DifyWorld } from './world' + +const e2eRoot = fileURLToPath(new URL('../..', import.meta.url)) +const artifactsDir = path.join(e2eRoot, 'cucumber-report', 'artifacts') + +let browser: Browser | undefined + +setDefaultTimeout(60_000) + +const sanitizeForPath = (value: string) => + value.replaceAll(/[^a-zA-Z0-9_-]+/g, '-').replaceAll(/^-+|-+$/g, '') + +const writeArtifact = async ( + scenarioName: string, + extension: 'html' | 'png', + contents: Buffer | string, +) => { + const artifactPath = path.join( + artifactsDir, + `${Date.now()}-${sanitizeForPath(scenarioName || 'scenario')}.${extension}`, + ) + await writeFile(artifactPath, contents) + + return artifactPath +} + +BeforeAll(async () => { + await mkdir(artifactsDir, { recursive: true }) + + browser = await chromium.launch({ + headless: cucumberHeadless, + slowMo: cucumberSlowMo, + }) + + console.log(`[e2e] session cache bootstrap against ${baseURL}`) + await ensureAuthenticatedState(browser, baseURL) +}) + +Before(async function (this: DifyWorld, { pickle }) { + if (!browser) throw new Error('Shared Playwright browser is not available.') + + await this.startAuthenticatedSession(browser) + this.scenarioStartedAt = Date.now() + + const tags = pickle.tags.map((tag) => tag.name).join(' ') + console.log(`[e2e] start ${pickle.name}${tags ? ` ${tags}` : ''}`) +}) + +After(async function (this: DifyWorld, { pickle, result }) { + const elapsedMs = this.scenarioStartedAt ? Date.now() - this.scenarioStartedAt : undefined + + if (result?.status !== Status.PASSED && this.page) { + const screenshot = await this.page.screenshot({ + fullPage: true, + }) + const screenshotPath = await writeArtifact(pickle.name, 'png', screenshot) + this.attach(screenshot, 'image/png') + + const html = await this.page.content() + const htmlPath = await writeArtifact(pickle.name, 'html', html) + this.attach(html, 'text/html') + + if (this.consoleErrors.length > 0) + this.attach(`Console Errors:\n${this.consoleErrors.join('\n')}`, 'text/plain') + + if (this.pageErrors.length > 0) + this.attach(`Page Errors:\n${this.pageErrors.join('\n')}`, 'text/plain') + + this.attach(`Artifacts:\n${[screenshotPath, htmlPath].join('\n')}`, 'text/plain') + } + + const status = result?.status || 'UNKNOWN' + console.log( + `[e2e] end ${pickle.name} status=${status}${elapsedMs ? ` durationMs=${elapsedMs}` : ''}`, + ) + + await this.closeSession() +}) + +AfterAll(async () => { + await browser?.close() + browser = undefined +}) diff --git a/e2e/features/support/world.ts b/e2e/features/support/world.ts new file mode 100644 index 00000000000..15ab8daf168 --- /dev/null +++ b/e2e/features/support/world.ts @@ -0,0 +1,68 @@ +import { type IWorldOptions, World, setWorldConstructor } from '@cucumber/cucumber' +import type { Browser, BrowserContext, ConsoleMessage, Page } from '@playwright/test' +import { + authStatePath, + readAuthSessionMetadata, + type AuthSessionMetadata, +} from '../../fixtures/auth' +import { baseURL, defaultLocale } from '../../test-env' + +export class DifyWorld extends World { + context: BrowserContext | undefined + page: Page | undefined + consoleErrors: string[] = [] + pageErrors: string[] = [] + scenarioStartedAt: number | undefined + session: AuthSessionMetadata | undefined + + constructor(options: IWorldOptions) { + super(options) + this.resetScenarioState() + } + + resetScenarioState() { + this.consoleErrors = [] + this.pageErrors = [] + } + + async startAuthenticatedSession(browser: Browser) { + this.resetScenarioState() + this.context = await browser.newContext({ + baseURL, + locale: defaultLocale, + storageState: authStatePath, + }) + this.context.setDefaultTimeout(30_000) + this.page = await this.context.newPage() + this.page.setDefaultTimeout(30_000) + + this.page.on('console', (message: ConsoleMessage) => { + if (message.type() === 'error') this.consoleErrors.push(message.text()) + }) + this.page.on('pageerror', (error) => { + this.pageErrors.push(error.message) + }) + } + + getPage() { + if (!this.page) throw new Error('Playwright page has not been initialized for this scenario.') + + return this.page + } + + async getAuthSession() { + this.session ??= await readAuthSessionMetadata() + return this.session + } + + async closeSession() { + await this.context?.close() + this.context = undefined + this.page = undefined + this.session = undefined + this.scenarioStartedAt = undefined + this.resetScenarioState() + } +} + +setWorldConstructor(DifyWorld) diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts new file mode 100644 index 00000000000..853bfff5ed8 --- /dev/null +++ b/e2e/fixtures/auth.ts @@ -0,0 +1,148 @@ +import type { Browser, Page } from '@playwright/test' +import { expect } from '@playwright/test' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defaultBaseURL, defaultLocale } from '../test-env' + +export type AuthSessionMetadata = { + adminEmail: string + baseURL: string + mode: 'install' | 'login' + usedInitPassword: boolean +} + +const WAIT_TIMEOUT_MS = 120_000 +const e2eRoot = fileURLToPath(new URL('..', import.meta.url)) + +export const authDir = path.join(e2eRoot, '.auth') +export const authStatePath = path.join(authDir, 'admin.json') +export const authMetadataPath = path.join(authDir, 'session.json') + +export const adminCredentials = { + email: process.env.E2E_ADMIN_EMAIL || 'e2e-admin@example.com', + name: process.env.E2E_ADMIN_NAME || 'E2E Admin', + password: process.env.E2E_ADMIN_PASSWORD || 'E2eAdmin12345', +} + +const initPassword = process.env.E2E_INIT_PASSWORD || 'E2eInit12345' + +export const resolveBaseURL = (configuredBaseURL?: string) => + configuredBaseURL || process.env.E2E_BASE_URL || defaultBaseURL + +export const readAuthSessionMetadata = async () => { + const content = await readFile(authMetadataPath, 'utf8') + return JSON.parse(content) as AuthSessionMetadata +} + +const escapeRegex = (value: string) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') + +const appURL = (baseURL: string, pathname: string) => new URL(pathname, baseURL).toString() + +const waitForPageState = async (page: Page) => { + const installHeading = page.getByRole('heading', { name: 'Setting up an admin account' }) + const signInButton = page.getByRole('button', { name: 'Sign in' }) + const initPasswordField = page.getByLabel('Admin initialization password') + + const deadline = Date.now() + WAIT_TIMEOUT_MS + + while (Date.now() < deadline) { + if (await installHeading.isVisible().catch(() => false)) return 'install' as const + if (await signInButton.isVisible().catch(() => false)) return 'login' as const + if (await initPasswordField.isVisible().catch(() => false)) return 'init' as const + + await page.waitForTimeout(1_000) + } + + throw new Error(`Unable to determine auth page state for ${page.url()}`) +} + +const completeInitPasswordIfNeeded = async (page: Page) => { + const initPasswordField = page.getByLabel('Admin initialization password') + if (!(await initPasswordField.isVisible({ timeout: 3_000 }).catch(() => false))) return false + + await initPasswordField.fill(initPassword) + await page.getByRole('button', { name: 'Validate' }).click() + await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({ + timeout: WAIT_TIMEOUT_MS, + }) + + return true +} + +const completeInstall = async (page: Page, baseURL: string) => { + await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({ + timeout: WAIT_TIMEOUT_MS, + }) + + await page.getByLabel('Email address').fill(adminCredentials.email) + await page.getByLabel('Username').fill(adminCredentials.name) + await page.getByLabel('Password').fill(adminCredentials.password) + await page.getByRole('button', { name: 'Set up' }).click() + + await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), { + timeout: WAIT_TIMEOUT_MS, + }) +} + +const completeLogin = async (page: Page, baseURL: string) => { + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible({ + timeout: WAIT_TIMEOUT_MS, + }) + + await page.getByLabel('Email address').fill(adminCredentials.email) + await page.getByLabel('Password').fill(adminCredentials.password) + await page.getByRole('button', { name: 'Sign in' }).click() + + await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), { + timeout: WAIT_TIMEOUT_MS, + }) +} + +export const ensureAuthenticatedState = async (browser: Browser, configuredBaseURL?: string) => { + const baseURL = resolveBaseURL(configuredBaseURL) + + await mkdir(authDir, { recursive: true }) + + const context = await browser.newContext({ + baseURL, + locale: defaultLocale, + }) + const page = await context.newPage() + + try { + await page.goto(appURL(baseURL, '/install'), { waitUntil: 'networkidle' }) + + let usedInitPassword = await completeInitPasswordIfNeeded(page) + let pageState = await waitForPageState(page) + + while (pageState === 'init') { + const completedInitPassword = await completeInitPasswordIfNeeded(page) + if (!completedInitPassword) + throw new Error(`Unable to validate initialization password for ${page.url()}`) + + usedInitPassword = true + pageState = await waitForPageState(page) + } + + if (pageState === 'install') await completeInstall(page, baseURL) + else await completeLogin(page, baseURL) + + await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible({ + timeout: WAIT_TIMEOUT_MS, + }) + + await context.storageState({ path: authStatePath }) + + const metadata: AuthSessionMetadata = { + adminEmail: adminCredentials.email, + baseURL, + mode: pageState, + usedInitPassword, + } + + await writeFile(authMetadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8') + } finally { + await context.close() + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000000..9b8a1f873fd --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,34 @@ +{ + "name": "dify-e2e", + "private": true, + "type": "module", + "scripts": { + "check": "vp check --fix", + "e2e": "tsx ./scripts/run-cucumber.ts", + "e2e:full": "tsx ./scripts/run-cucumber.ts --full", + "e2e:full:headed": "tsx ./scripts/run-cucumber.ts --full --headed", + "e2e:headed": "tsx ./scripts/run-cucumber.ts --headed", + "e2e:install": "playwright install --with-deps chromium", + "e2e:middleware:down": "tsx ./scripts/setup.ts middleware-down", + "e2e:middleware:up": "tsx ./scripts/setup.ts middleware-up", + "e2e:reset": "tsx ./scripts/setup.ts reset" + }, + "devDependencies": { + "@cucumber/cucumber": "12.7.0", + "@playwright/test": "1.51.1", + "@types/node": "25.5.0", + "tsx": "4.21.0", + "typescript": "5.9.3", + "vite-plus": "latest" + }, + "engines": { + "node": "^22.22.1" + }, + "packageManager": "pnpm@10.32.1", + "pnpm": { + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } + } +} diff --git a/e2e/pnpm-lock.yaml b/e2e/pnpm-lock.yaml new file mode 100644 index 00000000000..b63458ad4a0 --- /dev/null +++ b/e2e/pnpm-lock.yaml @@ -0,0 +1,2632 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + +importers: + + .: + devDependencies: + '@cucumber/cucumber': + specifier: 12.7.0 + version: 12.7.0 + '@playwright/test': + specifier: 1.51.1 + version: 1.51.1 + '@types/node': + specifier: 25.5.0 + version: 25.5.0 + tsx: + specifier: 4.21.0 + version: 4.21.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite-plus: + specifier: latest + version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cucumber/ci-environment@13.0.0': + resolution: {integrity: sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA==} + + '@cucumber/cucumber-expressions@19.0.0': + resolution: {integrity: sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg==} + + '@cucumber/cucumber@12.7.0': + resolution: {integrity: sha512-7A/9CJpJDxv1SQ7hAZU0zPn2yRxx6XMR+LO4T94Enm3cYNWsEEj+RGX38NLX4INT+H6w5raX3Csb/qs4vUBsOA==} + engines: {node: 20 || 22 || >=24} + hasBin: true + + '@cucumber/gherkin-streams@6.0.0': + resolution: {integrity: sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==} + hasBin: true + peerDependencies: + '@cucumber/gherkin': '>=22.0.0' + '@cucumber/message-streams': '>=4.0.0' + '@cucumber/messages': '>=17.1.1' + + '@cucumber/gherkin-utils@11.0.0': + resolution: {integrity: sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==} + hasBin: true + + '@cucumber/gherkin@38.0.0': + resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} + + '@cucumber/html-formatter@23.0.0': + resolution: {integrity: sha512-WwcRzdM8Ixy4e53j+Frm3fKM5rNuIyWUfy4HajEN+Xk/YcjA6yW0ACGTFDReB++VDZz/iUtwYdTlPRY36NbqJg==} + peerDependencies: + '@cucumber/messages': '>=18' + + '@cucumber/junit-xml-formatter@0.9.0': + resolution: {integrity: sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==} + peerDependencies: + '@cucumber/messages': '*' + + '@cucumber/message-streams@4.0.1': + resolution: {integrity: sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==} + peerDependencies: + '@cucumber/messages': '>=17.1.1' + + '@cucumber/messages@32.0.1': + resolution: {integrity: sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ==} + + '@cucumber/pretty-formatter@1.0.1': + resolution: {integrity: sha512-A1lU4VVP0aUWdOTmpdzvXOyEYuPtBDI0xYwYJnmoMDplzxMdhcHk86lyyvYDoMoPzzq6OkOE3isuosvUU4X7IQ==} + peerDependencies: + '@cucumber/cucumber': '>=7.0.0' + '@cucumber/messages': '*' + + '@cucumber/query@14.7.0': + resolution: {integrity: sha512-fiqZ4gMEgYjmbuWproF/YeCdD5y+gD2BqgBIGbpihOsx6UlNsyzoDSfO+Tny0q65DxfK+pHo2UkPyEl7dO7wmQ==} + peerDependencies: + '@cucumber/messages': '*' + + '@cucumber/tag-expressions@9.1.0': + resolution: {integrity: sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/runtime@0.121.0': + resolution: {integrity: sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@oxfmt/binding-android-arm-eabi@0.42.0': + resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.42.0': + resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.42.0': + resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.42.0': + resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.42.0': + resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': + resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': + resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.42.0': + resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.42.0': + resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': + resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': + resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.42.0': + resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.42.0': + resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.42.0': + resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.42.0': + resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.42.0': + resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.42.0': + resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.42.0': + resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.42.0': + resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.17.3': + resolution: {integrity: sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.17.3': + resolution: {integrity: sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.17.3': + resolution: {integrity: sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.17.3': + resolution: {integrity: sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.17.3': + resolution: {integrity: sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.17.3': + resolution: {integrity: sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.57.0': + resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.57.0': + resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.57.0': + resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.57.0': + resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.57.0': + resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.57.0': + resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.57.0': + resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.57.0': + resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.57.0': + resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.57.0': + resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.57.0': + resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.57.0': + resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.57.0': + resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.57.0': + resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.57.0': + resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.57.0': + resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.57.0': + resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.57.0': + resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.57.0': + resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@playwright/test@1.51.1': + resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==} + engines: {node: '>=18'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@teppeis/multimaps@3.0.0': + resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} + engines: {node: '>=14'} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@voidzero-dev/vite-plus-core@0.1.14': + resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.4 + '@tsdown/exe': 0.21.4 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 + unplugin-unused: ^0.5.0 + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': + resolution: {integrity: sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@voidzero-dev/vite-plus-darwin-x64@0.1.14': + resolution: {integrity: sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': + resolution: {integrity: sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': + resolution: {integrity: sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + resolution: {integrity: sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + resolution: {integrity: sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-test@0.1.14': + resolution: {integrity: sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/ui': 4.1.1 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': + resolution: {integrity: sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': + resolution: {integrity: sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error-formatter@3.0.0: + resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + has-ansi@4.0.1: + resolution: {integrity: sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==} + engines: {node: '>=8'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + knuth-shuffle-seeded@1.0.6: + resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + normalize-package-data@8.0.0: + resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + oxfmt@0.42.0: + resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint-tsgolint@0.17.3: + resolution: {integrity: sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==} + hasBin: true + + oxlint@1.57.0: + resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.15.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + pad-right@0.2.2: + resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} + engines: {node: '>=0.10.0'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pixelmatch@7.1.0: + resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} + hasBin: true + + playwright-core@1.51.1: + resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.51.1: + resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==} + engines: {node: '>=18'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + read-package-up@12.0.0: + resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} + engines: {node: '>=20'} + + read-pkg@10.1.0: + resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} + engines: {node: '>=20'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + regexp-match-indices@1.0.2: + resolution: {integrity: sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + seed-random@2.2.0: + resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + string-argv@0.3.1: + resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} + engines: {node: '>=20'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + + util-arity@1.1.0: + resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vite-plus@0.1.14: + resolution: {integrity: sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@colors/colors@1.5.0': + optional: true + + '@cucumber/ci-environment@13.0.0': {} + + '@cucumber/cucumber-expressions@19.0.0': + dependencies: + regexp-match-indices: 1.0.2 + + '@cucumber/cucumber@12.7.0': + dependencies: + '@cucumber/ci-environment': 13.0.0 + '@cucumber/cucumber-expressions': 19.0.0 + '@cucumber/gherkin': 38.0.0 + '@cucumber/gherkin-streams': 6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1) + '@cucumber/gherkin-utils': 11.0.0 + '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.0.1) + '@cucumber/junit-xml-formatter': 0.9.0(@cucumber/messages@32.0.1) + '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) + '@cucumber/messages': 32.0.1 + '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1) + '@cucumber/tag-expressions': 9.1.0 + assertion-error-formatter: 3.0.0 + capital-case: 1.0.4 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 14.0.3 + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 + figures: 3.2.0 + glob: 13.0.6 + has-ansi: 4.0.1 + indent-string: 4.0.0 + is-installed-globally: 0.4.0 + is-stream: 2.0.1 + knuth-shuffle-seeded: 1.0.6 + lodash.merge: 4.6.2 + lodash.mergewith: 4.6.2 + luxon: 3.7.2 + mime: 3.0.0 + mkdirp: 3.0.1 + mz: 2.7.0 + progress: 2.0.3 + read-package-up: 12.0.0 + semver: 7.7.4 + string-argv: 0.3.1 + supports-color: 8.1.1 + type-fest: 4.41.0 + util-arity: 1.1.0 + yaml: 2.8.3 + yup: 1.7.1 + + '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/gherkin': 38.0.0 + '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) + '@cucumber/messages': 32.0.1 + commander: 14.0.0 + source-map-support: 0.5.21 + + '@cucumber/gherkin-utils@11.0.0': + dependencies: + '@cucumber/gherkin': 38.0.0 + '@cucumber/messages': 32.0.1 + '@teppeis/multimaps': 3.0.0 + commander: 14.0.2 + source-map-support: 0.5.21 + + '@cucumber/gherkin@38.0.0': + dependencies: + '@cucumber/messages': 32.0.1 + + '@cucumber/html-formatter@23.0.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + + '@cucumber/junit-xml-formatter@0.9.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + '@cucumber/query': 14.7.0(@cucumber/messages@32.0.1) + '@teppeis/multimaps': 3.0.0 + luxon: 3.7.2 + xmlbuilder: 15.1.1 + + '@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + + '@cucumber/messages@32.0.1': + dependencies: + class-transformer: 0.5.1 + reflect-metadata: 0.2.2 + + '@cucumber/pretty-formatter@1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/cucumber': 12.7.0 + '@cucumber/messages': 32.0.1 + ansi-styles: 5.2.0 + cli-table3: 0.6.5 + figures: 3.2.0 + ts-dedent: 2.2.0 + + '@cucumber/query@14.7.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + '@teppeis/multimaps': 3.0.0 + lodash.sortby: 4.7.0 + + '@cucumber/tag-expressions@9.1.0': {} + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/runtime@0.121.0': {} + + '@oxc-project/types@0.122.0': {} + + '@oxfmt/binding-android-arm-eabi@0.42.0': + optional: true + + '@oxfmt/binding-android-arm64@0.42.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.42.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.42.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.42.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.42.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.42.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.42.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.42.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.42.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.42.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.42.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.42.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.42.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.42.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.17.3': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.17.3': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.17.3': + optional: true + + '@oxlint-tsgolint/linux-x64@0.17.3': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.17.3': + optional: true + + '@oxlint-tsgolint/win32-x64@0.17.3': + optional: true + + '@oxlint/binding-android-arm-eabi@1.57.0': + optional: true + + '@oxlint/binding-android-arm64@1.57.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.57.0': + optional: true + + '@oxlint/binding-darwin-x64@1.57.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.57.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.57.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.57.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.57.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.57.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.57.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.57.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.57.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.57.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.57.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.57.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.57.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.57.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.57.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.57.0': + optional: true + + '@playwright/test@1.51.1': + dependencies: + playwright: 1.51.1 + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@standard-schema/spec@1.1.0': {} + + '@teppeis/multimaps@3.0.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/normalize-package-data@2.4.4': {} + + '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + dependencies: + '@oxc-project/runtime': 0.121.0 + '@oxc-project/types': 0.122.0 + lightningcss: 1.32.0 + postcss: 8.5.8 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + tsx: 4.21.0 + typescript: 5.9.3 + yaml: 2.8.3 + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + es-module-lexer: 1.7.0 + obug: 2.1.1 + pixelmatch: 7.1.0 + pngjs: 7.0.0 + sirv: 3.0.2 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3) + ws: 8.20.0 + optionalDependencies: + '@types/node': 25.5.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@tsdown/css' + - '@tsdown/exe' + - '@vitejs/devtools' + - bufferutil + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - yaml + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': + optional: true + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + any-promise@1.3.0: {} + + assertion-error-formatter@3.0.0: + dependencies: + diff: 4.0.4 + pad-right: 0.2.2 + repeat-string: 1.6.1 + + assertion-error@2.0.1: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + buffer-from@1.1.2: {} + + cac@7.0.0: {} + + capital-case@1.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + class-transformer@0.5.1: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@14.0.0: {} + + commander@14.0.2: {} + + commander@14.0.3: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + detect-libc@2.1.2: {} + + diff@4.0.4: {} + + emoji-regex@8.0.0: {} + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-module-lexer@1.7.0: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escape-string-regexp@1.0.5: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + find-up-simple@1.0.1: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + has-ansi@4.0.1: + dependencies: + ansi-regex: 4.1.1 + + has-flag@4.0.0: {} + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.7 + + indent-string@4.0.0: {} + + index-to-position@1.2.0: {} + + ini@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-path-inside@3.0.3: {} + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + knuth-shuffle-seeded@1.0.6: + dependencies: + seed-random: 2.2.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.sortby@4.7.0: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@11.2.7: {} + + luxon@3.7.2: {} + + mime@3.0.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.5 + + minipass@7.1.3: {} + + mkdirp@3.0.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + normalize-package-data@8.0.0: + dependencies: + hosted-git-info: 9.0.2 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + + object-assign@4.1.1: {} + + obug@2.1.1: {} + + oxfmt@0.42.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.42.0 + '@oxfmt/binding-android-arm64': 0.42.0 + '@oxfmt/binding-darwin-arm64': 0.42.0 + '@oxfmt/binding-darwin-x64': 0.42.0 + '@oxfmt/binding-freebsd-x64': 0.42.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 + '@oxfmt/binding-linux-arm64-gnu': 0.42.0 + '@oxfmt/binding-linux-arm64-musl': 0.42.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 + '@oxfmt/binding-linux-riscv64-musl': 0.42.0 + '@oxfmt/binding-linux-s390x-gnu': 0.42.0 + '@oxfmt/binding-linux-x64-gnu': 0.42.0 + '@oxfmt/binding-linux-x64-musl': 0.42.0 + '@oxfmt/binding-openharmony-arm64': 0.42.0 + '@oxfmt/binding-win32-arm64-msvc': 0.42.0 + '@oxfmt/binding-win32-ia32-msvc': 0.42.0 + '@oxfmt/binding-win32-x64-msvc': 0.42.0 + + oxlint-tsgolint@0.17.3: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.17.3 + '@oxlint-tsgolint/darwin-x64': 0.17.3 + '@oxlint-tsgolint/linux-arm64': 0.17.3 + '@oxlint-tsgolint/linux-x64': 0.17.3 + '@oxlint-tsgolint/win32-arm64': 0.17.3 + '@oxlint-tsgolint/win32-x64': 0.17.3 + + oxlint@1.57.0(oxlint-tsgolint@0.17.3): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.57.0 + '@oxlint/binding-android-arm64': 1.57.0 + '@oxlint/binding-darwin-arm64': 1.57.0 + '@oxlint/binding-darwin-x64': 1.57.0 + '@oxlint/binding-freebsd-x64': 1.57.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.57.0 + '@oxlint/binding-linux-arm-musleabihf': 1.57.0 + '@oxlint/binding-linux-arm64-gnu': 1.57.0 + '@oxlint/binding-linux-arm64-musl': 1.57.0 + '@oxlint/binding-linux-ppc64-gnu': 1.57.0 + '@oxlint/binding-linux-riscv64-gnu': 1.57.0 + '@oxlint/binding-linux-riscv64-musl': 1.57.0 + '@oxlint/binding-linux-s390x-gnu': 1.57.0 + '@oxlint/binding-linux-x64-gnu': 1.57.0 + '@oxlint/binding-linux-x64-musl': 1.57.0 + '@oxlint/binding-openharmony-arm64': 1.57.0 + '@oxlint/binding-win32-arm64-msvc': 1.57.0 + '@oxlint/binding-win32-ia32-msvc': 1.57.0 + '@oxlint/binding-win32-x64-msvc': 1.57.0 + oxlint-tsgolint: 0.17.3 + + pad-right@0.2.2: + dependencies: + repeat-string: 1.6.1 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pixelmatch@7.1.0: + dependencies: + pngjs: 7.0.0 + + playwright-core@1.51.1: {} + + playwright@1.51.1: + dependencies: + playwright-core: 1.51.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + progress@2.0.3: {} + + property-expr@2.0.6: {} + + read-package-up@12.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 10.1.0 + type-fest: 5.5.0 + + read-pkg@10.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 8.0.0 + parse-json: 8.3.0 + type-fest: 5.5.0 + unicorn-magic: 0.4.0 + + reflect-metadata@0.2.2: {} + + regexp-match-indices@1.0.2: + dependencies: + regexp-tree: 0.1.27 + + regexp-tree@0.1.27: {} + + repeat-string@1.6.1: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown@1.0.0-rc.12: + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + + seed-random@2.2.0: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + stackframe@1.3.4: {} + + std-env@4.0.0: {} + + string-argv@0.3.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tagged-tag@1.0.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-case@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@2.1.0: {} + + toposort@2.0.2: {} + + totalist@3.0.1: {} + + ts-dedent@2.2.0: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@2.19.0: {} + + type-fest@4.41.0: {} + + type-fest@5.5.0: + dependencies: + tagged-tag: 1.0.0 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + unicorn-magic@0.4.0: {} + + upper-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + + util-arity@1.1.0: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vite-plus@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3): + dependencies: + '@oxc-project/types': 0.122.0 + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + cac: 7.0.0 + cross-spawn: 7.0.6 + oxfmt: 0.42.0 + oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + oxlint-tsgolint: 0.17.3 + picocolors: 1.1.1 + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@tsdown/css' + - '@tsdown/exe' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - vite + - yaml + + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + ws@8.20.0: {} + + xmlbuilder@15.1.1: {} + + yaml@2.8.3: {} + + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 diff --git a/e2e/scripts/common.ts b/e2e/scripts/common.ts new file mode 100644 index 00000000000..bb82121079d --- /dev/null +++ b/e2e/scripts/common.ts @@ -0,0 +1,242 @@ +import { spawn, type ChildProcess } from 'node:child_process' +import { access, copyFile, readFile, writeFile } from 'node:fs/promises' +import net from 'node:net' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { sleep } from '../support/process' + +type RunCommandOptions = { + command: string + args: string[] + cwd: string + env?: NodeJS.ProcessEnv + stdio?: 'inherit' | 'pipe' +} + +type RunCommandResult = { + exitCode: number + stdout: string + stderr: string +} + +type ForegroundProcessOptions = { + command: string + args: string[] + cwd: string + env?: NodeJS.ProcessEnv +} + +export const rootDir = fileURLToPath(new URL('../..', import.meta.url)) +export const e2eDir = path.join(rootDir, 'e2e') +export const apiDir = path.join(rootDir, 'api') +export const dockerDir = path.join(rootDir, 'docker') +export const webDir = path.join(rootDir, 'web') + +export const middlewareComposeFile = path.join(dockerDir, 'docker-compose.middleware.yaml') +export const middlewareEnvFile = path.join(dockerDir, 'middleware.env') +export const middlewareEnvExampleFile = path.join(dockerDir, 'middleware.env.example') +export const webEnvLocalFile = path.join(webDir, '.env.local') +export const webEnvExampleFile = path.join(webDir, '.env.example') +export const apiEnvExampleFile = path.join(apiDir, 'tests', 'integration_tests', '.env.example') + +const formatCommand = (command: string, args: string[]) => [command, ...args].join(' ') + +export const isMainModule = (metaUrl: string) => { + const entrypoint = process.argv[1] + if (!entrypoint) return false + + return pathToFileURL(entrypoint).href === metaUrl +} + +export const runCommand = async ({ + command, + args, + cwd, + env, + stdio = 'inherit', +}: RunCommandOptions): Promise => { + const childProcess = spawn(command, args, { + cwd, + env: { + ...process.env, + ...env, + }, + stdio: stdio === 'inherit' ? 'inherit' : 'pipe', + }) + + let stdout = '' + let stderr = '' + + if (stdio === 'pipe') { + childProcess.stdout?.on('data', (chunk: Buffer | string) => { + stdout += chunk.toString() + }) + childProcess.stderr?.on('data', (chunk: Buffer | string) => { + stderr += chunk.toString() + }) + } + + return await new Promise((resolve, reject) => { + childProcess.once('error', reject) + childProcess.once('exit', (code) => { + resolve({ + exitCode: code ?? 1, + stdout, + stderr, + }) + }) + }) +} + +export const runCommandOrThrow = async (options: RunCommandOptions) => { + const result = await runCommand(options) + + if (result.exitCode !== 0) { + throw new Error( + `Command failed (${result.exitCode}): ${formatCommand(options.command, options.args)}`, + ) + } + + return result +} + +const forwardSignalsToChild = (childProcess: ChildProcess) => { + const handleSignal = (signal: NodeJS.Signals) => { + if (childProcess.exitCode === null) childProcess.kill(signal) + } + + const onSigint = () => handleSignal('SIGINT') + const onSigterm = () => handleSignal('SIGTERM') + + process.on('SIGINT', onSigint) + process.on('SIGTERM', onSigterm) + + return () => { + process.off('SIGINT', onSigint) + process.off('SIGTERM', onSigterm) + } +} + +export const runForegroundProcess = async ({ + command, + args, + cwd, + env, +}: ForegroundProcessOptions) => { + const childProcess = spawn(command, args, { + cwd, + env: { + ...process.env, + ...env, + }, + stdio: 'inherit', + }) + + const cleanupSignals = forwardSignalsToChild(childProcess) + const exitCode = await new Promise((resolve, reject) => { + childProcess.once('error', reject) + childProcess.once('exit', (code) => { + resolve(code ?? 1) + }) + }) + + cleanupSignals() + process.exit(exitCode) +} + +export const ensureFileExists = async (filePath: string, exampleFilePath: string) => { + try { + await access(filePath) + } catch { + await copyFile(exampleFilePath, filePath) + } +} + +export const ensureLineInFile = async (filePath: string, line: string) => { + const fileContent = await readFile(filePath, 'utf8') + const lines = fileContent.split(/\r?\n/) + const assignmentPrefix = line.includes('=') ? `${line.slice(0, line.indexOf('='))}=` : null + + if (lines.includes(line)) return + + if (assignmentPrefix && lines.some((existingLine) => existingLine.startsWith(assignmentPrefix))) + return + + const normalizedContent = fileContent.endsWith('\n') ? fileContent : `${fileContent}\n` + await writeFile(filePath, `${normalizedContent}${line}\n`, 'utf8') +} + +export const ensureWebEnvLocal = async () => { + await ensureFileExists(webEnvLocalFile, webEnvExampleFile) + + const fileContent = await readFile(webEnvLocalFile, 'utf8') + const nextContent = fileContent.replaceAll('http://localhost:5001', 'http://127.0.0.1:5001') + + if (nextContent !== fileContent) await writeFile(webEnvLocalFile, nextContent, 'utf8') +} + +export const readSimpleDotenv = async (filePath: string) => { + const fileContent = await readFile(filePath, 'utf8') + const entries = fileContent + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map<[string, string]>((line) => { + const separatorIndex = line.indexOf('=') + const key = separatorIndex === -1 ? line : line.slice(0, separatorIndex).trim() + const rawValue = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1).trim() + + if ( + (rawValue.startsWith('"') && rawValue.endsWith('"')) || + (rawValue.startsWith("'") && rawValue.endsWith("'")) + ) { + return [key, rawValue.slice(1, -1)] + } + + return [key, rawValue] + }) + + return Object.fromEntries(entries) +} + +export const waitForCondition = async ({ + check, + description, + intervalMs, + timeoutMs, +}: { + check: () => Promise | boolean + description: string + intervalMs: number + timeoutMs: number +}) => { + const deadline = Date.now() + timeoutMs + + while (Date.now() < deadline) { + if (await check()) return + + await sleep(intervalMs) + } + + throw new Error(`Timed out waiting for ${description} after ${timeoutMs}ms.`) +} + +export const isTcpPortReachable = async (host: string, port: number, timeoutMs = 1_000) => { + return await new Promise((resolve) => { + const socket = net.createConnection({ + host, + port, + }) + + const finish = (result: boolean) => { + socket.removeAllListeners() + socket.destroy() + resolve(result) + } + + socket.setTimeout(timeoutMs) + socket.once('connect', () => finish(true)) + socket.once('timeout', () => finish(false)) + socket.once('error', () => finish(false)) + }) +} diff --git a/e2e/scripts/run-cucumber.ts b/e2e/scripts/run-cucumber.ts new file mode 100644 index 00000000000..39e91579164 --- /dev/null +++ b/e2e/scripts/run-cucumber.ts @@ -0,0 +1,154 @@ +import { mkdir, rm } from 'node:fs/promises' +import path from 'node:path' +import { startWebServer, stopWebServer } from '../support/web-server' +import { waitForUrl, startLoggedProcess, stopManagedProcess } from '../support/process' +import { apiURL, baseURL, reuseExistingWebServer } from '../test-env' +import { e2eDir, isMainModule, runCommand } from './common' +import { resetState, startMiddleware, stopMiddleware } from './setup' + +type RunOptions = { + forwardArgs: string[] + full: boolean + headed: boolean +} + +const parseArgs = (argv: string[]): RunOptions => { + let full = false + let headed = false + const forwardArgs: string[] = [] + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + + if (arg === '--') { + forwardArgs.push(...argv.slice(index + 1)) + break + } + + if (arg === '--full') { + full = true + continue + } + + if (arg === '--headed') { + headed = true + continue + } + + forwardArgs.push(arg) + } + + return { + forwardArgs, + full, + headed, + } +} + +const hasCustomTags = (forwardArgs: string[]) => + forwardArgs.some((arg) => arg === '--tags' || arg.startsWith('--tags=')) + +const main = async () => { + const { forwardArgs, full, headed } = parseArgs(process.argv.slice(2)) + const startMiddlewareForRun = full + const resetStateForRun = full + + if (resetStateForRun) await resetState() + + if (startMiddlewareForRun) await startMiddleware() + + const cucumberReportDir = path.join(e2eDir, 'cucumber-report') + const logDir = path.join(e2eDir, '.logs') + + await rm(cucumberReportDir, { force: true, recursive: true }) + await mkdir(logDir, { recursive: true }) + + const apiProcess = await startLoggedProcess({ + command: 'npx', + args: ['tsx', './scripts/setup.ts', 'api'], + cwd: e2eDir, + label: 'api server', + logFilePath: path.join(logDir, 'cucumber-api.log'), + }) + + let cleanupPromise: Promise | undefined + const cleanup = async () => { + if (!cleanupPromise) { + cleanupPromise = (async () => { + await stopWebServer() + await stopManagedProcess(apiProcess) + + if (startMiddlewareForRun) { + try { + await stopMiddleware() + } catch { + // Cleanup should continue even if middleware shutdown fails. + } + } + })() + } + + await cleanupPromise + } + + const onTerminate = () => { + void cleanup().finally(() => { + process.exit(1) + }) + } + + process.once('SIGINT', onTerminate) + process.once('SIGTERM', onTerminate) + + try { + try { + await waitForUrl(`${apiURL}/health`, 180_000, 1_000) + } catch { + throw new Error(`API did not become ready at ${apiURL}/health.`) + } + + await startWebServer({ + baseURL, + command: 'npx', + args: ['tsx', './scripts/setup.ts', 'web'], + cwd: e2eDir, + logFilePath: path.join(logDir, 'cucumber-web.log'), + reuseExistingServer: reuseExistingWebServer, + timeoutMs: 300_000, + }) + + const cucumberEnv: NodeJS.ProcessEnv = { + ...process.env, + CUCUMBER_HEADLESS: headed ? '0' : '1', + } + + if (startMiddlewareForRun && !hasCustomTags(forwardArgs)) + cucumberEnv.E2E_CUCUMBER_TAGS = 'not @skip' + + const result = await runCommand({ + command: 'npx', + args: [ + 'tsx', + './node_modules/@cucumber/cucumber/bin/cucumber.js', + '--config', + './cucumber.config.ts', + ...forwardArgs, + ], + cwd: e2eDir, + env: cucumberEnv, + }) + + process.exitCode = result.exitCode + } finally { + process.off('SIGINT', onTerminate) + process.off('SIGTERM', onTerminate) + await cleanup() + } +} + +if (isMainModule(import.meta.url)) { + void main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) + }) +} diff --git a/e2e/scripts/setup.ts b/e2e/scripts/setup.ts new file mode 100644 index 00000000000..6f38598df4e --- /dev/null +++ b/e2e/scripts/setup.ts @@ -0,0 +1,306 @@ +import { access, mkdir, rm } from 'node:fs/promises' +import path from 'node:path' +import { waitForUrl } from '../support/process' +import { + apiDir, + apiEnvExampleFile, + dockerDir, + e2eDir, + ensureFileExists, + ensureLineInFile, + ensureWebEnvLocal, + isMainModule, + isTcpPortReachable, + middlewareComposeFile, + middlewareEnvExampleFile, + middlewareEnvFile, + readSimpleDotenv, + runCommand, + runCommandOrThrow, + runForegroundProcess, + waitForCondition, + webDir, +} from './common' + +const buildIdPath = path.join(webDir, '.next', 'BUILD_ID') + +const middlewareDataPaths = [ + path.join(dockerDir, 'volumes', 'db', 'data'), + path.join(dockerDir, 'volumes', 'plugin_daemon'), + path.join(dockerDir, 'volumes', 'redis', 'data'), + path.join(dockerDir, 'volumes', 'weaviate'), +] + +const e2eStatePaths = [ + path.join(e2eDir, '.auth'), + path.join(e2eDir, 'cucumber-report'), + path.join(e2eDir, '.logs'), + path.join(e2eDir, 'playwright-report'), + path.join(e2eDir, 'test-results'), +] + +const composeArgs = [ + 'compose', + '-f', + middlewareComposeFile, + '--profile', + 'postgresql', + '--profile', + 'weaviate', +] + +const getApiEnvironment = async () => { + const envFromExample = await readSimpleDotenv(apiEnvExampleFile) + + return { + ...envFromExample, + FLASK_APP: 'app.py', + } +} + +const getServiceContainerId = async (service: string) => { + const result = await runCommandOrThrow({ + command: 'docker', + args: ['compose', '-f', middlewareComposeFile, 'ps', '-q', service], + cwd: dockerDir, + stdio: 'pipe', + }) + + return result.stdout.trim() +} + +const getContainerHealth = async (containerId: string) => { + const result = await runCommand({ + command: 'docker', + args: ['inspect', '-f', '{{.State.Health.Status}}', containerId], + cwd: dockerDir, + stdio: 'pipe', + }) + + if (result.exitCode !== 0) return '' + + return result.stdout.trim() +} + +const printComposeLogs = async (services: string[]) => { + await runCommand({ + command: 'docker', + args: ['compose', '-f', middlewareComposeFile, 'logs', ...services], + cwd: dockerDir, + }) +} + +const waitForDependency = async ({ + description, + services, + wait, +}: { + description: string + services: string[] + wait: () => Promise +}) => { + console.log(`Waiting for ${description}...`) + + try { + await wait() + } catch (error) { + await printComposeLogs(services) + throw error + } +} + +export const ensureWebBuild = async () => { + await ensureWebEnvLocal() + + if (process.env.E2E_FORCE_WEB_BUILD === '1') { + await runCommandOrThrow({ + command: 'pnpm', + args: ['run', 'build'], + cwd: webDir, + }) + return + } + + try { + await access(buildIdPath) + console.log('Reusing existing web build artifact.') + } catch { + await runCommandOrThrow({ + command: 'pnpm', + args: ['run', 'build'], + cwd: webDir, + }) + } +} + +export const startWeb = async () => { + await ensureWebBuild() + + await runForegroundProcess({ + command: 'pnpm', + args: ['run', 'start'], + cwd: webDir, + env: { + HOSTNAME: '127.0.0.1', + PORT: '3000', + }, + }) +} + +export const startApi = async () => { + const env = await getApiEnvironment() + + await runCommandOrThrow({ + command: 'uv', + args: ['run', '--project', '.', 'flask', 'upgrade-db'], + cwd: apiDir, + env, + }) + + await runForegroundProcess({ + command: 'uv', + args: ['run', '--project', '.', 'flask', 'run', '--host', '127.0.0.1', '--port', '5001'], + cwd: apiDir, + env, + }) +} + +export const stopMiddleware = async () => { + await runCommandOrThrow({ + command: 'docker', + args: [...composeArgs, 'down', '--remove-orphans'], + cwd: dockerDir, + }) +} + +export const resetState = async () => { + console.log('Stopping middleware services...') + try { + await stopMiddleware() + } catch { + // Reset should continue even if middleware is already stopped. + } + + console.log('Removing persisted middleware data...') + await Promise.all( + middlewareDataPaths.map(async (targetPath) => { + await rm(targetPath, { force: true, recursive: true }) + await mkdir(targetPath, { recursive: true }) + }), + ) + + console.log('Removing E2E local state...') + await Promise.all( + e2eStatePaths.map((targetPath) => rm(targetPath, { force: true, recursive: true })), + ) + + console.log('E2E state reset complete.') +} + +export const startMiddleware = async () => { + await ensureFileExists(middlewareEnvFile, middlewareEnvExampleFile) + await ensureLineInFile(middlewareEnvFile, 'COMPOSE_PROFILES=postgresql,weaviate') + + console.log('Starting middleware services...') + await runCommandOrThrow({ + command: 'docker', + args: [ + ...composeArgs, + 'up', + '-d', + 'db_postgres', + 'redis', + 'weaviate', + 'sandbox', + 'ssrf_proxy', + 'plugin_daemon', + ], + cwd: dockerDir, + }) + + const [postgresContainerId, redisContainerId] = await Promise.all([ + getServiceContainerId('db_postgres'), + getServiceContainerId('redis'), + ]) + + await waitForDependency({ + description: 'PostgreSQL and Redis health checks', + services: ['db_postgres', 'redis'], + wait: () => + waitForCondition({ + check: async () => { + const [postgresStatus, redisStatus] = await Promise.all([ + getContainerHealth(postgresContainerId), + getContainerHealth(redisContainerId), + ]) + + return postgresStatus === 'healthy' && redisStatus === 'healthy' + }, + description: 'PostgreSQL and Redis health checks', + intervalMs: 2_000, + timeoutMs: 240_000, + }), + }) + + await waitForDependency({ + description: 'Weaviate readiness', + services: ['weaviate'], + wait: () => waitForUrl('http://127.0.0.1:8080/v1/.well-known/ready', 120_000, 2_000), + }) + + await waitForDependency({ + description: 'sandbox health', + services: ['sandbox', 'ssrf_proxy'], + wait: () => waitForUrl('http://127.0.0.1:8194/health', 120_000, 2_000), + }) + + await waitForDependency({ + description: 'plugin daemon port', + services: ['plugin_daemon'], + wait: () => + waitForCondition({ + check: async () => isTcpPortReachable('127.0.0.1', 5002), + description: 'plugin daemon port', + intervalMs: 2_000, + timeoutMs: 120_000, + }), + }) + + console.log('Full middleware stack is ready.') +} + +const printUsage = () => { + console.log('Usage: tsx ./scripts/setup.ts ') +} + +const main = async () => { + const command = process.argv[2] + + switch (command) { + case 'api': + await startApi() + return + case 'middleware-down': + await stopMiddleware() + return + case 'middleware-up': + await startMiddleware() + return + case 'reset': + await resetState() + return + case 'web': + await startWeb() + return + default: + printUsage() + process.exitCode = 1 + } +} + +if (isMainModule(import.meta.url)) { + void main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) + }) +} diff --git a/e2e/support/process.ts b/e2e/support/process.ts new file mode 100644 index 00000000000..96273ef9312 --- /dev/null +++ b/e2e/support/process.ts @@ -0,0 +1,178 @@ +import type { ChildProcess } from 'node:child_process' +import { spawn } from 'node:child_process' +import { createWriteStream, type WriteStream } from 'node:fs' +import { mkdir } from 'node:fs/promises' +import net from 'node:net' +import { dirname } from 'node:path' + +type ManagedProcessOptions = { + command: string + args?: string[] + cwd: string + env?: NodeJS.ProcessEnv + label: string + logFilePath: string +} + +export type ManagedProcess = { + childProcess: ChildProcess + label: string + logFilePath: string + logStream: WriteStream +} + +export const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + +export const isPortReachable = async (host: string, port: number, timeoutMs = 1_000) => { + return await new Promise((resolve) => { + const socket = net.createConnection({ + host, + port, + }) + + const finish = (result: boolean) => { + socket.removeAllListeners() + socket.destroy() + resolve(result) + } + + socket.setTimeout(timeoutMs) + socket.once('connect', () => finish(true)) + socket.once('timeout', () => finish(false)) + socket.once('error', () => finish(false)) + }) +} + +export const waitForUrl = async ( + url: string, + timeoutMs: number, + intervalMs = 1_000, + requestTimeoutMs = Math.max(intervalMs, 1_000), +) => { + const deadline = Date.now() + timeoutMs + + while (Date.now() < deadline) { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), requestTimeoutMs) + + try { + const response = await fetch(url, { + signal: controller.signal, + }) + if (response.ok) return + } finally { + clearTimeout(timeout) + } + } catch { + // Keep polling until timeout. + } + + await sleep(intervalMs) + } + + throw new Error(`Timed out waiting for ${url} after ${timeoutMs}ms.`) +} + +export const startLoggedProcess = async ({ + command, + args = [], + cwd, + env, + label, + logFilePath, +}: ManagedProcessOptions): Promise => { + await mkdir(dirname(logFilePath), { recursive: true }) + + const logStream = createWriteStream(logFilePath, { flags: 'a' }) + const childProcess = spawn(command, args, { + cwd, + env: { + ...process.env, + ...env, + }, + detached: process.platform !== 'win32', + stdio: ['ignore', 'pipe', 'pipe'], + }) + + const formattedCommand = [command, ...args].join(' ') + logStream.write(`[${new Date().toISOString()}] Starting ${label}: ${formattedCommand}\n`) + childProcess.stdout?.pipe(logStream, { end: false }) + childProcess.stderr?.pipe(logStream, { end: false }) + + return { + childProcess, + label, + logFilePath, + logStream, + } +} + +const waitForProcessExit = (childProcess: ChildProcess, timeoutMs: number) => + new Promise((resolve) => { + if (childProcess.exitCode !== null) { + resolve() + return + } + + const timeout = setTimeout(() => { + cleanup() + resolve() + }, timeoutMs) + + const onExit = () => { + cleanup() + resolve() + } + + const cleanup = () => { + clearTimeout(timeout) + childProcess.off('exit', onExit) + } + + childProcess.once('exit', onExit) + }) + +const signalManagedProcess = (childProcess: ChildProcess, signal: NodeJS.Signals) => { + const { pid } = childProcess + if (!pid) return + + try { + if (process.platform !== 'win32') { + process.kill(-pid, signal) + return + } + + childProcess.kill(signal) + } catch { + // Best-effort shutdown. Cleanup continues even when the process is already gone. + } +} + +export const stopManagedProcess = async (managedProcess?: ManagedProcess) => { + if (!managedProcess) return + + const { childProcess, logStream } = managedProcess + + if (childProcess.exitCode === null) { + signalManagedProcess(childProcess, 'SIGTERM') + await waitForProcessExit(childProcess, 5_000) + } + + if (childProcess.exitCode === null) { + signalManagedProcess(childProcess, 'SIGKILL') + await waitForProcessExit(childProcess, 5_000) + } + + childProcess.stdout?.unpipe(logStream) + childProcess.stderr?.unpipe(logStream) + childProcess.stdout?.destroy() + childProcess.stderr?.destroy() + + await new Promise((resolve) => { + logStream.end(() => resolve()) + }) +} diff --git a/e2e/support/web-server.ts b/e2e/support/web-server.ts new file mode 100644 index 00000000000..ad5d5d916a1 --- /dev/null +++ b/e2e/support/web-server.ts @@ -0,0 +1,83 @@ +import type { ManagedProcess } from './process' +import { isPortReachable, startLoggedProcess, stopManagedProcess, waitForUrl } from './process' + +type WebServerStartOptions = { + baseURL: string + command: string + args?: string[] + cwd: string + logFilePath: string + reuseExistingServer: boolean + timeoutMs: number +} + +let activeProcess: ManagedProcess | undefined + +const getUrlHostAndPort = (url: string) => { + const parsedUrl = new URL(url) + const isHttps = parsedUrl.protocol === 'https:' + + return { + host: parsedUrl.hostname, + port: parsedUrl.port ? Number(parsedUrl.port) : isHttps ? 443 : 80, + } +} + +export const startWebServer = async ({ + baseURL, + command, + args = [], + cwd, + logFilePath, + reuseExistingServer, + timeoutMs, +}: WebServerStartOptions) => { + const { host, port } = getUrlHostAndPort(baseURL) + + if (reuseExistingServer && (await isPortReachable(host, port))) return + + activeProcess = await startLoggedProcess({ + command, + args, + cwd, + label: 'web server', + logFilePath, + }) + + let startupError: Error | undefined + activeProcess.childProcess.once('error', (error) => { + startupError = error + }) + activeProcess.childProcess.once('exit', (code, signal) => { + if (startupError) return + + startupError = new Error( + `Web server exited before readiness (code: ${code ?? 'unknown'}, signal: ${signal ?? 'none'}).`, + ) + }) + + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (startupError) { + await stopManagedProcess(activeProcess) + activeProcess = undefined + throw startupError + } + + try { + await waitForUrl(baseURL, 1_000, 250, 1_000) + return + } catch { + // Continue polling until timeout or child exit. + } + } + + await stopManagedProcess(activeProcess) + activeProcess = undefined + throw new Error(`Timed out waiting for web server readiness at ${baseURL} after ${timeoutMs}ms.`) +} + +export const stopWebServer = async () => { + await stopManagedProcess(activeProcess) + activeProcess = undefined +} diff --git a/e2e/test-env.ts b/e2e/test-env.ts new file mode 100644 index 00000000000..c0afc2a8c1f --- /dev/null +++ b/e2e/test-env.ts @@ -0,0 +1,12 @@ +export const defaultBaseURL = 'http://127.0.0.1:3000' +export const defaultApiURL = 'http://127.0.0.1:5001' +export const defaultLocale = 'en-US' + +export const baseURL = process.env.E2E_BASE_URL || defaultBaseURL +export const apiURL = process.env.E2E_API_URL || defaultApiURL + +export const cucumberHeadless = process.env.CUCUMBER_HEADLESS !== '0' +export const cucumberSlowMo = Number(process.env.E2E_SLOW_MO || 0) +export const reuseExistingWebServer = process.env.E2E_REUSE_WEB_SERVER + ? process.env.E2E_REUSE_WEB_SERVER !== '0' + : !process.env.CI diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 00000000000..3976c12b667 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": false, + "resolveJsonModule": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["node", "@playwright/test", "@cucumber/cucumber"], + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": ["./**/*.ts"], + "exclude": [ + "./node_modules", + "./playwright-report", + "./test-results", + "./.auth", + "./cucumber-report", + "./.logs" + ] +} diff --git a/e2e/vite.config.ts b/e2e/vite.config.ts new file mode 100644 index 00000000000..98400d5b9b6 --- /dev/null +++ b/e2e/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite-plus' + +export default defineConfig({ + lint: { + options: { + typeAware: true, + typeCheck: true, + denyWarnings: true, + }, + }, + fmt: { + singleQuote: true, + semi: false, + }, +})