diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml
index c1da73b5df..7bce056970 100644
--- a/.github/workflows/api-tests.yml
+++ b/.github/workflows/api-tests.yml
@@ -14,11 +14,11 @@ concurrency:
cancel-in-progress: true
jobs:
- test:
- name: API Tests
+ api-unit:
+ name: API Unit Tests
runs-on: ubuntu-latest
env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ COVERAGE_FILE: coverage-unit
defaults:
run:
shell: bash
@@ -50,6 +50,52 @@ jobs:
- name: Run dify config tests
run: uv run --project api dev/pytest/pytest_config_tests.py
+ - name: Run Unit Tests
+ run: uv run --project api bash dev/pytest/pytest_unit_tests.sh
+
+ - name: Upload unit coverage data
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: api-coverage-unit
+ path: coverage-unit
+ retention-days: 1
+
+ api-integration:
+ name: API Integration Tests
+ runs-on: ubuntu-latest
+ env:
+ COVERAGE_FILE: coverage-integration
+ STORAGE_TYPE: opendal
+ OPENDAL_SCHEME: fs
+ OPENDAL_FS_ROOT: /tmp/dify-storage
+ defaults:
+ run:
+ shell: bash
+ strategy:
+ matrix:
+ python-version:
+ - "3.12"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Setup UV and Python
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
+ with:
+ enable-cache: true
+ python-version: ${{ matrix.python-version }}
+ cache-dependency-glob: api/uv.lock
+
+ - name: Check UV lockfile
+ run: uv lock --project api --check
+
+ - name: Install dependencies
+ run: uv sync --project api --dev
+
- name: Set up dotenvs
run: |
cp docker/.env.example docker/.env
@@ -73,22 +119,90 @@ jobs:
run: |
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
- - name: Run API Tests
- env:
- STORAGE_TYPE: opendal
- OPENDAL_SCHEME: fs
- OPENDAL_FS_ROOT: /tmp/dify-storage
+ - name: Run Integration Tests
run: |
uv run --project api pytest \
-n auto \
--timeout "${PYTEST_TIMEOUT:-180}" \
api/tests/integration_tests/workflow \
api/tests/integration_tests/tools \
- api/tests/test_containers_integration_tests \
- api/tests/unit_tests
+ api/tests/test_containers_integration_tests
+
+ - name: Upload integration coverage data
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: api-coverage-integration
+ path: coverage-integration
+ retention-days: 1
+
+ api-coverage:
+ name: API Coverage
+ runs-on: ubuntu-latest
+ needs:
+ - api-unit
+ - api-integration
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ COVERAGE_FILE: .coverage
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - 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 dependencies
+ run: uv sync --project api --dev
+
+ - name: Download coverage data
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ path: coverage-data
+ pattern: api-coverage-*
+ merge-multiple: true
+
+ - name: Combine coverage
+ run: |
+ set -euo pipefail
+
+ echo "### API Coverage" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+
+ unit_coverage="$(find coverage-data -type f -name coverage-unit -print -quit)"
+ integration_coverage="$(find coverage-data -type f -name coverage-integration -print -quit)"
+ : "${unit_coverage:?coverage-unit artifact not found}"
+ : "${integration_coverage:?coverage-integration artifact not found}"
+
+ report_file="$(mktemp)"
+ uv run --project api coverage combine "$unit_coverage" "$integration_coverage"
+ uv run --project api coverage report --show-missing | tee "$report_file"
+ echo "Summary: \`$(tail -n 1 "$report_file")\`" >> "$GITHUB_STEP_SUMMARY"
+ {
+ echo ""
+ echo "Coverage report
"
+ echo ""
+ echo '```'
+ cat "$report_file"
+ echo '```'
+ echo " "
+ } >> "$GITHUB_STEP_SUMMARY"
+ uv run --project api coverage xml -o coverage.xml
- name: Report coverage
- if: ${{ env.CODECOV_TOKEN != '' && matrix.python-version == '3.12' }}
+ if: ${{ env.CODECOV_TOKEN != '' }}
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
files: ./coverage.xml
diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index 6fffbefce0..2d96dae4da 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 }}
@@ -53,21 +54,63 @@ jobs:
filters: |
api:
- 'api/**'
- - 'docker/**'
- '.github/workflows/api-tests.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/middleware.env.example'
+ - 'docker/docker-compose.middleware.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/volumes/sandbox/conf/**'
web:
- '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/**'
+ - 'api/tests/integration_tests/vdb/**'
- '.github/workflows/vdb-tests.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/middleware.env.example'
+ - 'docker/docker-compose.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/certbot/**'
+ - 'docker/couchbase-server/**'
+ - 'docker/elasticsearch/**'
+ - 'docker/iris/**'
+ - 'docker/nginx/**'
+ - 'docker/pgvector/**'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/startupscripts/**'
+ - 'docker/tidb/**'
+ - 'docker/volumes/**'
- 'api/uv.lock'
- 'api/pyproject.toml'
migration:
- 'api/migrations/**'
+ - 'api/.env.example'
- '.github/workflows/db-migration-test.yml'
+ - '.github/workflows/expose_service_ports.sh'
+ - 'docker/.env.example'
+ - 'docker/middleware.env.example'
+ - 'docker/docker-compose.middleware.yaml'
+ - 'docker/docker-compose-template.yaml'
+ - 'docker/generate_docker_compose'
+ - 'docker/ssrf_proxy/**'
+ - 'docker/volumes/sandbox/conf/**'
# Run tests in parallel while always emitting stable required checks.
api-tests-run:
@@ -190,6 +233,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 0000000000..8035d1ef8e
--- /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/api/tests/test_containers_integration_tests/services/test_workflow_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_service.py
index ee7b68e6aa..b5ce8a53de 100644
--- a/api/tests/test_containers_integration_tests/services/test_workflow_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_workflow_service.py
@@ -555,6 +555,124 @@ class TestWorkflowService:
assert len(result_workflows) == 2
assert all(wf.marked_name for wf in result_workflows)
+ def test_get_all_published_workflow_no_workflow_id(self, db_session_with_containers: Session):
+ """Test that an app with no workflow_id returns empty results."""
+ # Arrange
+ fake = Faker()
+ app = self._create_test_app(db_session_with_containers, fake)
+ app.workflow_id = None
+ db_session_with_containers.commit()
+
+ workflow_service = WorkflowService()
+
+ # Act
+ result_workflows, has_more = workflow_service.get_all_published_workflow(
+ session=db_session_with_containers, app_model=app, page=1, limit=10, user_id=None
+ )
+
+ # Assert
+ assert result_workflows == []
+ assert has_more is False
+
+ def test_get_all_published_workflow_basic(self, db_session_with_containers: Session):
+ """Test basic retrieval of published workflows."""
+ # Arrange
+ fake = Faker()
+ account = self._create_test_account(db_session_with_containers, fake)
+ app = self._create_test_app(db_session_with_containers, fake)
+
+ workflow1 = self._create_test_workflow(db_session_with_containers, app, account, fake)
+ workflow1.version = "2024.01.01.001"
+ workflow2 = self._create_test_workflow(db_session_with_containers, app, account, fake)
+ workflow2.version = "2024.01.02.001"
+
+ app.workflow_id = workflow1.id
+ db_session_with_containers.commit()
+
+ workflow_service = WorkflowService()
+
+ # Act
+ result_workflows, has_more = workflow_service.get_all_published_workflow(
+ session=db_session_with_containers, app_model=app, page=1, limit=10, user_id=None
+ )
+
+ # Assert
+ assert len(result_workflows) == 2
+ assert has_more is False
+
+ def test_get_all_published_workflow_combined_filters(self, db_session_with_containers: Session):
+ """Test combined user_id and named_only filters."""
+ # Arrange
+ fake = Faker()
+ account1 = self._create_test_account(db_session_with_containers, fake)
+ account2 = self._create_test_account(db_session_with_containers, fake)
+ app = self._create_test_app(db_session_with_containers, fake)
+
+ # account1 named
+ wf1 = self._create_test_workflow(db_session_with_containers, app, account1, fake)
+ wf1.version = "2024.01.01.001"
+ wf1.marked_name = "Named by user1"
+ wf1.created_by = account1.id
+
+ # account1 unnamed
+ wf2 = self._create_test_workflow(db_session_with_containers, app, account1, fake)
+ wf2.version = "2024.01.02.001"
+ wf2.marked_name = ""
+ wf2.created_by = account1.id
+
+ # account2 named
+ wf3 = self._create_test_workflow(db_session_with_containers, app, account2, fake)
+ wf3.version = "2024.01.03.001"
+ wf3.marked_name = "Named by user2"
+ wf3.created_by = account2.id
+
+ app.workflow_id = wf1.id
+ db_session_with_containers.commit()
+
+ workflow_service = WorkflowService()
+
+ # Act - Filter by account1 + named_only
+ result_workflows, has_more = workflow_service.get_all_published_workflow(
+ session=db_session_with_containers,
+ app_model=app,
+ page=1,
+ limit=10,
+ user_id=account1.id,
+ named_only=True,
+ )
+
+ # Assert - Only wf1 matches (account1 + named)
+ assert len(result_workflows) == 1
+ assert result_workflows[0].marked_name == "Named by user1"
+ assert result_workflows[0].created_by == account1.id
+
+ def test_get_all_published_workflow_empty_result(self, db_session_with_containers: Session):
+ """Test that querying with no matching workflows returns empty."""
+ # Arrange
+ fake = Faker()
+ account = self._create_test_account(db_session_with_containers, fake)
+ app = self._create_test_app(db_session_with_containers, fake)
+
+ # Create a draft workflow (no version set = draft)
+ workflow = self._create_test_workflow(db_session_with_containers, app, account, fake)
+ app.workflow_id = workflow.id
+ db_session_with_containers.commit()
+
+ workflow_service = WorkflowService()
+
+ # Act - Filter by a user that has no workflows
+ result_workflows, has_more = workflow_service.get_all_published_workflow(
+ session=db_session_with_containers,
+ app_model=app,
+ page=1,
+ limit=10,
+ user_id="00000000-0000-0000-0000-000000000000",
+ )
+
+ # Assert
+ assert result_workflows == []
+ assert has_more is False
+
def test_sync_draft_workflow_create_new(self, db_session_with_containers: Session):
"""
Test creating a new draft workflow through sync operation.
diff --git a/api/tests/unit_tests/services/workflow/test_workflow_service.py b/api/tests/unit_tests/services/workflow/test_workflow_service.py
deleted file mode 100644
index b9d097350b..0000000000
--- a/api/tests/unit_tests/services/workflow/test_workflow_service.py
+++ /dev/null
@@ -1,415 +0,0 @@
-from contextlib import nullcontext
-from types import SimpleNamespace
-from unittest.mock import MagicMock
-
-import pytest
-from graphon.entities.graph_config import NodeConfigDictAdapter
-from graphon.enums import BuiltinNodeTypes
-from graphon.nodes.human_input.entities import FormInput, HumanInputNodeData, UserAction
-from graphon.nodes.human_input.enums import FormInputType
-
-from models.model import App
-from models.workflow import Workflow
-from services import workflow_service as workflow_service_module
-from services.workflow_service import WorkflowService
-
-
-class TestWorkflowService:
- @pytest.fixture
- def workflow_service(self):
- mock_session_maker = MagicMock()
- return WorkflowService(mock_session_maker)
-
- @pytest.fixture
- def mock_app(self):
- app = MagicMock(spec=App)
- app.id = "app-id-1"
- app.workflow_id = "workflow-id-1"
- app.tenant_id = "tenant-id-1"
- return app
-
- @pytest.fixture
- def mock_workflows(self):
- workflows = []
- for i in range(5):
- workflow = MagicMock(spec=Workflow)
- workflow.id = f"workflow-id-{i}"
- workflow.app_id = "app-id-1"
- workflow.created_at = f"2023-01-0{5 - i}" # Descending date order
- workflow.created_by = "user-id-1" if i % 2 == 0 else "user-id-2"
- workflow.marked_name = f"Workflow {i}" if i % 2 == 0 else ""
- workflows.append(workflow)
- return workflows
-
- @pytest.fixture
- def dummy_session_cls(self):
- class DummySession:
- def __init__(self, *args, **kwargs):
- self.commit = MagicMock()
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc, tb):
- return False
-
- def begin(self):
- return nullcontext()
-
- return DummySession
-
- def test_get_all_published_workflow_no_workflow_id(self, workflow_service, mock_app):
- mock_app.workflow_id = None
- mock_session = MagicMock()
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=10, user_id=None
- )
-
- assert workflows == []
- assert has_more is False
- mock_session.scalars.assert_not_called()
-
- def test_get_all_published_workflow_basic(self, workflow_service, mock_app, mock_workflows):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- mock_scalar_result.all.return_value = mock_workflows[:3]
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=3, user_id=None
- )
-
- assert workflows == mock_workflows[:3]
- assert has_more is False
- mock_session.scalars.assert_called_once()
-
- def test_get_all_published_workflow_pagination(self, workflow_service, mock_app, mock_workflows):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- # Return 4 items when limit is 3, which should indicate has_more=True
- mock_scalar_result.all.return_value = mock_workflows[:4]
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=3, user_id=None
- )
-
- # Should return only the first 3 items
- assert len(workflows) == 3
- assert workflows == mock_workflows[:3]
- assert has_more is True
-
- # Test page 2
- mock_scalar_result.all.return_value = mock_workflows[3:]
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=2, limit=3, user_id=None
- )
-
- assert len(workflows) == 2
- assert has_more is False
-
- def test_get_all_published_workflow_user_filter(self, workflow_service, mock_app, mock_workflows):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- # Filter workflows for user-id-1
- filtered_workflows = [w for w in mock_workflows if w.created_by == "user-id-1"]
- mock_scalar_result.all.return_value = filtered_workflows
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=10, user_id="user-id-1"
- )
-
- assert workflows == filtered_workflows
- assert has_more is False
- mock_session.scalars.assert_called_once()
-
- # Verify that the select contains a user filter clause
- args = mock_session.scalars.call_args[0][0]
- assert "created_by" in str(args)
-
- def test_get_all_published_workflow_named_only(self, workflow_service, mock_app, mock_workflows):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- # Filter workflows that have a marked_name
- named_workflows = [w for w in mock_workflows if w.marked_name]
- mock_scalar_result.all.return_value = named_workflows
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=10, user_id=None, named_only=True
- )
-
- assert workflows == named_workflows
- assert has_more is False
- mock_session.scalars.assert_called_once()
-
- # Verify that the select contains a named_only filter clause
- args = mock_session.scalars.call_args[0][0]
- assert "marked_name !=" in str(args)
-
- def test_get_all_published_workflow_combined_filters(self, workflow_service, mock_app, mock_workflows):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- # Combined filter: user-id-1 and has marked_name
- filtered_workflows = [w for w in mock_workflows if w.created_by == "user-id-1" and w.marked_name]
- mock_scalar_result.all.return_value = filtered_workflows
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=10, user_id="user-id-1", named_only=True
- )
-
- assert workflows == filtered_workflows
- assert has_more is False
- mock_session.scalars.assert_called_once()
-
- # Verify that both filters are applied
- args = mock_session.scalars.call_args[0][0]
- assert "created_by" in str(args)
- assert "marked_name !=" in str(args)
-
- def test_get_all_published_workflow_empty_result(self, workflow_service, mock_app):
- mock_session = MagicMock()
- mock_scalar_result = MagicMock()
- mock_scalar_result.all.return_value = []
- mock_session.scalars.return_value = mock_scalar_result
-
- workflows, has_more = workflow_service.get_all_published_workflow(
- session=mock_session, app_model=mock_app, page=1, limit=10, user_id=None
- )
-
- assert workflows == []
- assert has_more is False
- mock_session.scalars.assert_called_once()
-
- def test_submit_human_input_form_preview_uses_rendered_content(
- self,
- workflow_service: WorkflowService,
- monkeypatch: pytest.MonkeyPatch,
- dummy_session_cls,
- ) -> None:
- service = workflow_service
- node_data = HumanInputNodeData(
- title="Human Input",
- form_content="
{{#$output.name#}}
",
- inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
- user_actions=[UserAction(id="approve", title="Approve")],
- )
- node = MagicMock()
- node.node_data = node_data
- node.render_form_content_before_submission.return_value = "preview
"
- node.render_form_content_with_outputs.return_value = "rendered
"
-
- service._build_human_input_variable_pool = MagicMock(return_value=MagicMock()) # type: ignore[method-assign]
- service._build_human_input_node = MagicMock(return_value=node) # type: ignore[method-assign]
-
- workflow = MagicMock()
- node_config = NodeConfigDictAdapter.validate_python(
- {"id": "node-1", "data": {"type": BuiltinNodeTypes.HUMAN_INPUT}}
- )
- workflow.get_node_config_by_id.return_value = node_config
- workflow.get_enclosing_node_type_and_id.return_value = None
- service.get_draft_workflow = MagicMock(return_value=workflow) # type: ignore[method-assign]
-
- saved_outputs: dict[str, object] = {}
-
- class DummySaver:
- def __init__(self, *args, **kwargs):
- pass
-
- def save(self, outputs, process_data):
- saved_outputs.update(outputs)
-
- monkeypatch.setattr(workflow_service_module, "Session", dummy_session_cls)
- monkeypatch.setattr(workflow_service_module, "DraftVariableSaver", DummySaver)
- monkeypatch.setattr(workflow_service_module, "db", SimpleNamespace(engine=MagicMock()))
-
- app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
- account = SimpleNamespace(id="account-1")
-
- result = service.submit_human_input_form_preview(
- app_model=app_model,
- account=account,
- node_id="node-1",
- form_inputs={"name": "Ada", "extra": "ignored"},
- inputs={"#node-0.result#": "LLM output"},
- action="approve",
- )
-
- service._build_human_input_variable_pool.assert_called_once_with(
- app_model=app_model,
- workflow=workflow,
- node_config=node_config,
- manual_inputs={"#node-0.result#": "LLM output"},
- user_id="account-1",
- )
-
- node.render_form_content_with_outputs.assert_called_once()
- called_args = node.render_form_content_with_outputs.call_args.args
- assert called_args[0] == "preview
"
- assert called_args[2] == node_data.outputs_field_names()
- rendered_outputs = called_args[1]
- assert rendered_outputs["name"] == "Ada"
- assert rendered_outputs["extra"] == "ignored"
- assert "extra" in saved_outputs
- assert "extra" in result
- assert saved_outputs["name"] == "Ada"
- assert result["name"] == "Ada"
- assert result["__action_id"] == "approve"
- assert "__rendered_content" in result
-
- def test_submit_human_input_form_preview_missing_inputs_message(self, workflow_service: WorkflowService) -> None:
- service = workflow_service
- node_data = HumanInputNodeData(
- title="Human Input",
- form_content="{{#$output.name#}}
",
- inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
- user_actions=[UserAction(id="approve", title="Approve")],
- )
- node = MagicMock()
- node.node_data = node_data
- node._render_form_content_before_submission.return_value = "preview
"
- node._render_form_content_with_outputs.return_value = "rendered
"
-
- service._build_human_input_variable_pool = MagicMock(return_value=MagicMock()) # type: ignore[method-assign]
- service._build_human_input_node = MagicMock(return_value=node) # type: ignore[method-assign]
-
- workflow = MagicMock()
- workflow.get_node_config_by_id.return_value = NodeConfigDictAdapter.validate_python(
- {"id": "node-1", "data": {"type": BuiltinNodeTypes.HUMAN_INPUT}}
- )
- service.get_draft_workflow = MagicMock(return_value=workflow) # type: ignore[method-assign]
-
- app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
- account = SimpleNamespace(id="account-1")
- with pytest.raises(ValueError) as exc_info:
- service.submit_human_input_form_preview(
- app_model=app_model,
- account=account,
- node_id="node-1",
- form_inputs={},
- inputs={},
- action="approve",
- )
-
- assert "Missing required inputs" in str(exc_info.value)
-
- def test_run_draft_workflow_node_successful_behavior(
- self, workflow_service, mock_app, monkeypatch, dummy_session_cls
- ):
- """Behavior: When a basic workflow node runs, it correctly sets up context,
- executes the node, and saves outputs."""
- service = workflow_service
- account = SimpleNamespace(id="account-1")
- mock_workflow = MagicMock()
- mock_workflow.id = "wf-1"
- mock_workflow.tenant_id = "tenant-1"
- mock_workflow.environment_variables = []
- mock_workflow.conversation_variables = []
-
- # Mock node config
- mock_workflow.get_node_config_by_id.return_value = NodeConfigDictAdapter.validate_python(
- {"id": "node-1", "data": {"type": BuiltinNodeTypes.LLM}}
- )
- mock_workflow.get_enclosing_node_type_and_id.return_value = None
-
- # Mock class methods
- monkeypatch.setattr(workflow_service_module, "WorkflowDraftVariableService", MagicMock())
- monkeypatch.setattr(workflow_service_module, "DraftVarLoader", MagicMock())
-
- # Mock workflow entry execution
- mock_node_exec = MagicMock()
- mock_node_exec.id = "exec-1"
- mock_node_exec.process_data = {}
- mock_run = MagicMock()
- monkeypatch.setattr(workflow_service_module.WorkflowEntry, "single_step_run", mock_run)
-
- # Mock execution handling
- service._handle_single_step_result = MagicMock(return_value=mock_node_exec)
-
- # Mock repository
- mock_repo = MagicMock()
- mock_repo.get_execution_by_id.return_value = mock_node_exec
- mock_repo_factory = MagicMock(return_value=mock_repo)
- monkeypatch.setattr(
- workflow_service_module.DifyCoreRepositoryFactory,
- "create_workflow_node_execution_repository",
- mock_repo_factory,
- )
- service._node_execution_service_repo = mock_repo
-
- # Set up node execution service repo mock to return our exec node
- mock_node_exec.load_full_outputs.return_value = {"output_var": "result_value"}
- mock_node_exec.node_id = "node-1"
- mock_node_exec.node_type = "llm"
-
- # Mock draft variable saver
- mock_saver = MagicMock()
- monkeypatch.setattr(workflow_service_module, "DraftVariableSaver", MagicMock(return_value=mock_saver))
-
- # Mock DB
- monkeypatch.setattr(workflow_service_module, "db", SimpleNamespace(engine=MagicMock()))
-
- monkeypatch.setattr(workflow_service_module, "Session", dummy_session_cls)
-
- # Act
- result = service.run_draft_workflow_node(
- app_model=mock_app,
- draft_workflow=mock_workflow,
- node_id="node-1",
- user_inputs={"input_val": "test"},
- account=account,
- )
-
- # Assert
- assert result == mock_node_exec
- service._handle_single_step_result.assert_called_once()
- mock_repo.save.assert_called_once_with(mock_node_exec)
- mock_saver.save.assert_called_once_with(process_data={}, outputs={"output_var": "result_value"})
-
- def test_run_draft_workflow_node_failure_behavior(self, workflow_service, mock_app, monkeypatch, dummy_session_cls):
- """Behavior: If retrieving the saved execution fails, an appropriate error bubble matches expectations."""
- service = workflow_service
- account = SimpleNamespace(id="account-1")
- mock_workflow = MagicMock()
- mock_workflow.tenant_id = "tenant-1"
- mock_workflow.environment_variables = []
- mock_workflow.conversation_variables = []
- mock_workflow.get_node_config_by_id.return_value = NodeConfigDictAdapter.validate_python(
- {"id": "node-1", "data": {"type": BuiltinNodeTypes.LLM}}
- )
- mock_workflow.get_enclosing_node_type_and_id.return_value = None
-
- monkeypatch.setattr(workflow_service_module, "WorkflowDraftVariableService", MagicMock())
- monkeypatch.setattr(workflow_service_module, "DraftVarLoader", MagicMock())
- monkeypatch.setattr(workflow_service_module.WorkflowEntry, "single_step_run", MagicMock())
-
- mock_node_exec = MagicMock()
- mock_node_exec.id = "exec-invalid"
- service._handle_single_step_result = MagicMock(return_value=mock_node_exec)
-
- mock_repo = MagicMock()
- mock_repo_factory = MagicMock(return_value=mock_repo)
- monkeypatch.setattr(
- workflow_service_module.DifyCoreRepositoryFactory,
- "create_workflow_node_execution_repository",
- mock_repo_factory,
- )
- service._node_execution_service_repo = mock_repo
-
- # Simulate failure to retrieve the saved execution
- mock_repo.get_execution_by_id.return_value = None
-
- monkeypatch.setattr(workflow_service_module, "db", SimpleNamespace(engine=MagicMock()))
-
- monkeypatch.setattr(workflow_service_module, "Session", dummy_session_cls)
-
- # Act & Assert
- with pytest.raises(ValueError, match="WorkflowNodeExecution with id exec-invalid not found after saving"):
- service.run_draft_workflow_node(
- app_model=mock_app, draft_workflow=mock_workflow, node_id="node-1", user_inputs={}, account=account
- )
diff --git a/dev/pytest/pytest_unit_tests.sh b/dev/pytest/pytest_unit_tests.sh
index a034083304..1d4ff4d86f 100755
--- a/dev/pytest/pytest_unit_tests.sh
+++ b/dev/pytest/pytest_unit_tests.sh
@@ -13,4 +13,4 @@ PYTEST_XDIST_ARGS="${PYTEST_XDIST_ARGS:--n auto}"
pytest --timeout "${PYTEST_TIMEOUT}" ${PYTEST_XDIST_ARGS} api/tests/unit_tests --ignore=api/tests/unit_tests/controllers
# Run controller tests sequentially to avoid import race conditions
-pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests/controllers
+pytest --timeout "${PYTEST_TIMEOUT}" --cov-append api/tests/unit_tests/controllers
diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml
index 1746bb567a..911da70a73 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 0000000000..96c1e0f3a1
--- /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 0000000000..245c9863d4
--- /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 0000000000..9b4046eaff
--- /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 0000000000..c162a6562e
--- /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 0000000000..c0ca8ea4e0
--- /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 0000000000..3c1191a330
--- /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 0000000000..39fc1f996b
--- /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 0000000000..b8e76c6f06
--- /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 0000000000..bf03c2d8f4
--- /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 0000000000..b18ff035fa
--- /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 0000000000..857e01a971
--- /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 0000000000..a6862d79f5
--- /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 0000000000..15ab8daf16
--- /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 0000000000..853bfff5ed
--- /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 0000000000..9b8a1f873f
--- /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 0000000000..b63458ad4a
--- /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 0000000000..bb82121079
--- /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 0000000000..39e9157916
--- /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 0000000000..6f38598df4
--- /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 0000000000..96273ef931
--- /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 0000000000..ad5d5d916a
--- /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 0000000000..c0afc2a8c1
--- /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 0000000000..3976c12b66
--- /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 0000000000..98400d5b9b
--- /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,
+ },
+})
diff --git a/web/package.json b/web/package.json
index 76cfac4eba..65372ef5f5 100644
--- a/web/package.json
+++ b/web/package.json
@@ -220,7 +220,7 @@
"eslint-plugin-react-refresh": "0.5.2",
"eslint-plugin-sonarjs": "4.0.2",
"eslint-plugin-storybook": "10.3.1",
- "happy-dom": "20.8.8",
+ "happy-dom": "20.8.9",
"hono": "4.12.8",
"husky": "9.1.7",
"iconify-import-svg": "0.1.2",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 8fd930e2b8..cd1a8a8556 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -371,7 +371,7 @@ importers:
devDependencies:
'@antfu/eslint-config':
specifier: 7.7.3
- version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3)
+ version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3)
'@chromatic-com/storybook':
specifier: 5.0.2
version: 5.0.2(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
@@ -506,7 +506,7 @@ importers:
version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)
'@vitest/coverage-v8':
specifier: 4.1.0
- version: 4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
+ version: 4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
agentation:
specifier: 2.3.3
version: 2.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -547,8 +547,8 @@ importers:
specifier: 10.3.1
version: 10.3.1(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
happy-dom:
- specifier: 20.8.8
- version: 20.8.8
+ specifier: 20.8.9
+ version: 20.8.9
hono:
specifier: 4.12.8
version: 4.12.8
@@ -605,13 +605,13 @@ importers:
version: 11.3.3(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
vite-plus:
specifier: 0.1.13
- version: 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
+ version: 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
vitest:
specifier: npm:@voidzero-dev/vite-plus-test@0.1.13
- version: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+ version: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
vitest-canvas-mock:
specifier: 1.1.3
- version: 1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
+ version: 1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
packages:
@@ -768,12 +768,12 @@ packages:
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
- '@asamuzakjp/css-color@5.0.1':
- resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==}
+ '@asamuzakjp/css-color@5.1.1':
+ resolution: {integrity: sha512-iGWN8E45Ws0XWx3D44Q1t6vX2LqhCKcwfmwBYCDsFrYFS6m4q/Ks61L2veETaLv+ckDC6+dTETJoaAAb7VjLiw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
- '@asamuzakjp/dom-selector@7.0.3':
- resolution: {integrity: sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==}
+ '@asamuzakjp/dom-selector@7.0.4':
+ resolution: {integrity: sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/nwsapi@2.3.9':
@@ -957,8 +957,8 @@ packages:
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
- '@csstools/css-syntax-patches-for-csstree@1.1.1':
- resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
+ '@csstools/css-syntax-patches-for-csstree@1.1.2':
+ resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==}
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
@@ -5356,8 +5356,8 @@ packages:
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
- happy-dom@20.8.8:
- resolution: {integrity: sha512-5/F8wxkNxYtsN0bXfMwIyNLZ9WYsoOYPbmoluqVJqv8KBUbcyKZawJ7uYK4WTX8IHBLYv+VXIwfeNDPy1oKMwQ==}
+ happy-dom@20.8.9:
+ resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==}
engines: {node: '>=20.0.0'}
has-flag@4.0.0:
@@ -7313,6 +7313,10 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ tapable@2.3.2:
+ resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
+ engines: {node: '>=6'}
+
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
@@ -7532,8 +7536,8 @@ packages:
resolution: {integrity: sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==}
engines: {node: '>=20.18.1'}
- undici@7.24.5:
- resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
+ undici@7.24.6:
+ resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==}
engines: {node: '>=20.18.1'}
unicode-trie@2.0.0:
@@ -7680,7 +7684,7 @@ packages:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vinext@https://pkg.pr.new/vinext@b6a2cac:
- resolution: {integrity: sha512-/Jm507qqC1dCOhCaorb9H8/I5JEqkcsiUJw0Wgprg7Znym4eyLUvcWcRLVyM9z22Tm0+O1PugcSDA8oNvbqPuQ==, tarball: https://pkg.pr.new/vinext@b6a2cac}
+ resolution: {tarball: https://pkg.pr.new/vinext@b6a2cac}
version: 0.0.5
engines: {node: '>=22'}
hasBin: true
@@ -7882,6 +7886,18 @@ packages:
utf-8-validate:
optional: 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
+
wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
@@ -8141,7 +8157,7 @@ snapshots:
idb: 8.0.0
tslib: 2.8.1
- '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3)':
+ '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 1.1.0
@@ -8151,7 +8167,7 @@ snapshots:
'@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@1.21.7))
'@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)
- '@vitest/eslint-plugin': 1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)
+ '@vitest/eslint-plugin': 1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)
ansis: 4.2.0
cac: 7.0.0
eslint: 10.1.0(jiti@1.21.7)
@@ -8211,7 +8227,7 @@ snapshots:
'@antfu/utils@8.1.1': {}
- '@asamuzakjp/css-color@5.0.1':
+ '@asamuzakjp/css-color@5.1.1':
dependencies:
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
@@ -8220,7 +8236,7 @@ snapshots:
lru-cache: 11.2.7
optional: true
- '@asamuzakjp/dom-selector@7.0.3':
+ '@asamuzakjp/dom-selector@7.0.4':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
@@ -8480,7 +8496,7 @@ snapshots:
'@csstools/css-tokenizer': 4.0.0
optional: true
- '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
+ '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
optional: true
@@ -10368,7 +10384,7 @@ snapshots:
'@tanstack/devtools-event-bus@0.4.1':
dependencies:
- ws: 8.19.0
+ ws: 8.20.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -11037,7 +11053,7 @@ snapshots:
optionalDependencies:
react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))
- '@vitest/coverage-v8@4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))':
+ '@vitest/coverage-v8@4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.0
@@ -11049,16 +11065,16 @@ snapshots:
obug: 2.1.1
std-env: 4.0.0
tinyrainbow: 3.1.0
- vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+ vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
- '@vitest/eslint-plugin@1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)':
+ '@vitest/eslint-plugin@1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)
eslint: 10.1.0(jiti@1.21.7)
optionalDependencies:
typescript: 5.9.3
- vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+ vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
transitivePeerDependencies:
- supports-color
@@ -11123,7 +11139,7 @@ snapshots:
'@voidzero-dev/vite-plus-linux-x64-gnu@0.1.13':
optional: true
- '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)':
+ '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
@@ -11141,7 +11157,7 @@ snapshots:
ws: 8.19.0
optionalDependencies:
'@types/node': 25.5.0
- happy-dom: 20.8.8
+ happy-dom: 20.8.9
jsdom: 29.0.1(canvas@3.2.2)
transitivePeerDependencies:
- '@arethetypeswrong/core'
@@ -12915,14 +12931,14 @@ snapshots:
hachure-fill@0.5.2: {}
- happy-dom@20.8.8:
+ happy-dom@20.8.9:
dependencies:
'@types/node': 25.5.0
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
entities: 7.0.1
whatwg-mimetype: 3.0.0
- ws: 8.19.0
+ ws: 8.20.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -13295,10 +13311,10 @@ snapshots:
jsdom@29.0.1(canvas@3.2.2):
dependencies:
- '@asamuzakjp/css-color': 5.0.1
- '@asamuzakjp/dom-selector': 7.0.3
+ '@asamuzakjp/css-color': 5.1.1
+ '@asamuzakjp/dom-selector': 7.0.4
'@bramus/specificity': 2.4.2
- '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
+ '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1)
'@exodus/bytes': 1.15.0
css-tree: 3.2.1
data-urls: 7.0.0
@@ -13310,7 +13326,7 @@ snapshots:
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.1
- undici: 7.24.5
+ undici: 7.24.6
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
@@ -15474,6 +15490,8 @@ snapshots:
tapable@2.3.0: {}
+ tapable@2.3.2: {}
+
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
@@ -15688,7 +15706,7 @@ snapshots:
undici@7.24.0: {}
- undici@7.24.5:
+ undici@7.24.6:
optional: true
unicode-trie@2.0.0:
@@ -15913,11 +15931,11 @@ snapshots:
- supports-color
- typescript
- vite-plus@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
+ vite-plus@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@oxc-project/types': 0.120.0
'@voidzero-dev/vite-plus-core': 0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
- '@voidzero-dev/vite-plus-test': 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
+ '@voidzero-dev/vite-plus-test': 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
cac: 7.0.0
cross-spawn: 7.0.6
oxfmt: 0.41.0
@@ -15984,11 +16002,11 @@ snapshots:
optionalDependencies:
vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
- vitest-canvas-mock@1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)):
+ vitest-canvas-mock@1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)):
dependencies:
cssfontparser: 1.2.1
moo-color: 1.0.3
- vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.8)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+ vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
void-elements@3.1.0: {}
@@ -16067,7 +16085,7 @@ snapshots:
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 4.3.3
- tapable: 2.3.0
+ tapable: 2.3.2
terser-webpack-plugin: 5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))
watchpack: 2.5.1
webpack-sources: 3.3.4
@@ -16112,6 +16130,8 @@ snapshots:
ws@8.19.0: {}
+ ws@8.20.0: {}
+
wsl-utils@0.1.0:
dependencies:
is-wsl: 3.1.1