diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..ecb242a --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,80 @@ +name: Nightly compatibility + +on: + schedule: + - cron: '17 18 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nightly-compatibility + cancel-in-progress: false + +# The reusable gate supplies the complete Ubuntu runtime matrix. These jobs add +# Windows/macOS across every declared runtime. As of 2026-08-28, Node 18/20 are +# EOL compatibility lines; 22/24 are LTS and 26 is Current. Evidence: +# https://github.com/nodejs/Release#release-schedule +jobs: + quality: + name: Reusable Ubuntu quality matrix + uses: ./.github/workflows/reusable-quality.yml + permissions: + contents: read + + legacy-os-runtime: + name: Legacy / ${{ matrix.os }} / Node ${{ matrix.node }} + needs: quality + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + node: [18.18.0, 20, 22, 24, 26] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.quality.outputs.package_artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Run the node:test Legacy controller against the exact tarball + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + HTTP_PROXY: '' + HTTPS_PROXY: '' + ALL_PROXY: '' + NO_PROXY: 127.0.0.1,localhost + run: node --test packages/network-debugger/test/e2e/pack/legacy-runtime.test.mjs + + native-os-runtime: + name: Native / ${{ matrix.os }} / Node ${{ matrix.node }} + needs: quality + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + node: [22, 24, 26] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.quality.outputs.package_artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Run the node:test Native controller against the exact tarball + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + HTTP_PROXY: '' + HTTPS_PROXY: '' + ALL_PROXY: '' + NO_PROXY: 127.0.0.1,localhost + run: node --test packages/network-debugger/test/e2e/pack/native-runtime.test.mjs diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b9540fa..f1c751d 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -4,20 +4,58 @@ on: release: types: [published] +permissions: + contents: read + id-token: write + +concurrency: + group: npm-publish-${{ github.event.release.tag_name }} + cancel-in-progress: false + jobs: + quality: + name: Rebuild once and run the reusable release gate + uses: ./.github/workflows/reusable-quality.yml + permissions: + contents: read + publish: + name: Publish the exact tested tarball + needs: quality runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + # npm trusted publishing requires npm >=11.5.1 and Node >=22.14. The + # official configuration is OIDC-first; NPM_TOKEN remains a migration + # fallback but cannot bypass the reusable quality dependency above. + # https://docs.npmjs.com/trusted-publishers/ + - uses: actions/setup-node@v4 with: - node-version: 22 - registry-url: 'https://registry.npmjs.org/' - - run: npm i -g pnpm@9.12.2 - - run: pnpm i - - run: pnpm build --filter=node-network-devtools - - run: | - cd packages/network-debugger - npm publish --access public + node-version: 24 + registry-url: https://registry.npmjs.org/ + - run: npm install --global npm@11 + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.quality.outputs.package_artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Verify release candidate identity + shell: bash + env: + EXPECTED_TARBALL: ${{ needs.quality.outputs.package_tarball_name }} + EXPECTED_SHA256: ${{ needs.quality.outputs.package_sha256 }} + EXPECTED_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + tarball="$RUNNER_TEMP/npm-package/$EXPECTED_TARBALL" + test -f "$tarball" + echo "$EXPECTED_SHA256 $tarball" | sha256sum --check --strict + package_version="$(tar -xOf "$tarball" package/package.json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version")" + test "${EXPECTED_TAG#v}" = "$package_version" + - name: Publish with OIDC provenance (or NPM_TOKEN fallback) env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + TARBALL: ${{ runner.temp }}/npm-package/${{ needs.quality.outputs.package_tarball_name }} + run: npm publish "$TARBALL" --access public --provenance diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..ae60928 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,34 @@ +name: Quality + +on: + pull_request: + push: + branches: + - main + - 'codex/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: quality-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Required package and protocol quality + uses: ./.github/workflows/reusable-quality.yml + permissions: + contents: read + + quality-gate: + name: Quality Gate + if: always() + needs: quality + runs-on: ubuntu-latest + steps: + - name: Enforce the complete reusable workflow result + env: + QUALITY_RESULT: ${{ needs.quality.result }} + run: test "$QUALITY_RESULT" = success diff --git a/.github/workflows/reusable-quality.yml b/.github/workflows/reusable-quality.yml new file mode 100644 index 0000000..fb2ed22 --- /dev/null +++ b/.github/workflows/reusable-quality.yml @@ -0,0 +1,299 @@ +name: Reusable package quality + +on: + workflow_call: + outputs: + package_artifact_name: + description: Artifact containing the one tested npm tarball + value: ${{ jobs.package.outputs.artifact_name }} + package_tarball_name: + description: Exact npm tarball filename + value: ${{ jobs.package.outputs.tarball_name }} + package_sha256: + description: SHA-256 of the exact npm tarball + value: ${{ jobs.package.outputs.sha256 }} + +permissions: + contents: read + +# Matrix reviewed 2026-08-28 against the official Node.js release schedule: +# https://github.com/nodejs/Release#release-schedule +# Node 22/24 are LTS and Node 26 is Current. Node 18 (EOL 2025-04-30) and +# Node 20 (EOL 2026-04-30) remain compatibility lines: Node 20 runs unit and +# Legacy evidence, while Node 18 is Legacy-only. Every runtime controller below +# is explicit -- +# unsupported capability combinations are not silently skipped at runtime. +env: + HTTP_PROXY: '' + HTTPS_PROXY: '' + ALL_PROXY: '' + NO_PROXY: 127.0.0.1,localhost + NETWORK_DEBUGGER_E2E_ARTIFACT_DIR: packages/network-debugger/test/e2e/artifacts + +jobs: + unit-supported: + name: Unit / Ubuntu / Node ${{ matrix.node }}${{ matrix.lifecycle == 'eol' && ' (EOL compatibility)' || '' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - node: 20 + lifecycle: eol + - node: 22 + lifecycle: supported + - node: 24 + lifecycle: supported + - node: 26 + lifecycle: supported + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm --filter node-network-devtools test:unit + + package: + name: Build and npm pack exactly once + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + artifact_name: ${{ steps.pack.outputs.artifact_name }} + tarball_name: ${{ steps.pack.outputs.tarball_name }} + sha256: ${{ steps.pack.outputs.sha256 }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm --filter node-network-devtools build + - name: Create the sole release candidate tarball + id: pack + shell: bash + run: | + set -euo pipefail + package_dir="$RUNNER_TEMP/npm-package" + mkdir -p "$package_dir" + npm pack --pack-destination "$package_dir" ./packages/network-debugger + mapfile -t tarballs < <(find "$package_dir" -maxdepth 1 -type f -name '*.tgz' -print) + if [[ "${#tarballs[@]}" -ne 1 ]]; then + echo "Expected exactly one npm tarball, found ${#tarballs[@]}" >&2 + exit 1 + fi + tarball_name="$(basename "${tarballs[0]}")" + sha256="$(sha256sum "${tarballs[0]}" | cut -d ' ' -f 1)" + artifact_name="node-network-devtools-tgz-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + echo "artifact_name=$artifact_name" >> "$GITHUB_OUTPUT" + echo "tarball_name=$tarball_name" >> "$GITHUB_OUTPUT" + echo "sha256=$sha256" >> "$GITHUB_OUTPUT" + - name: Upload the exact release candidate + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.pack.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package/*.tgz + if-no-files-found: error + compression-level: 0 + retention-days: 14 + + pack-consumer: + name: Packed CJS and ESM consumers / Node 24 + needs: package + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Resolve public entries from an installed tarball + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + run: node --test packages/network-debugger/test/e2e/pack/pack-consumer.test.mjs + + legacy-runtime: + name: Legacy runtime / Ubuntu / Node ${{ matrix.node }}${{ matrix.lifecycle == 'eol' && ' (EOL compatibility)' || '' }} + needs: package + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - node: 18.18.0 + lifecycle: eol + - node: 20 + lifecycle: eol + - node: 22 + lifecycle: supported + - node: 24 + lifecycle: supported + - node: 26 + lifecycle: supported + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + # This controller intentionally uses node:test + npm only. In particular, + # Node 18/20 compatibility never loads the current Vitest toolchain. + - name: Exercise a real packed Legacy CDP lifecycle + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + run: node --test packages/network-debugger/test/e2e/pack/legacy-runtime.test.mjs + + native-runtime: + name: Native packed runtime / Ubuntu / Node ${{ matrix.node }} + needs: package + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node: [22, 24, 26] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Exercise a real packed Native CDP lifecycle + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + run: node --test packages/network-debugger/test/e2e/pack/native-runtime.test.mjs + + ubuntu-protocol: + name: Mandatory Ubuntu Native, Legacy, and CLI protocol / Node 24 + needs: package + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Hydrate repository tests from the exact tarball + shell: bash + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + run: | + set -euo pipefail + tarball="$(find "$NND_PACK_DIR" -maxdepth 1 -type f -name '*.tgz' -print -quit)" + unpacked="$RUNNER_TEMP/npm-unpacked" + mkdir -p "$unpacked" + tar -xzf "$tarball" -C "$unpacked" + rm -rf packages/network-debugger/dist + cp -R "$unpacked/package/dist" packages/network-debugger/dist + - run: pnpm --filter node-network-devtools test:e2e:native + - run: pnpm --filter node-network-devtools test:e2e:legacy + - run: pnpm --filter node-network-devtools test:e2e:enhancements + - run: pnpm --filter node-network-devtools test:e2e:cli + - name: Upload Native, Legacy, and CLI protocol diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: native-legacy-cli-protocol-node-24 + path: packages/network-debugger/test/e2e/artifacts + if-no-files-found: ignore + + adapter-os-smoke: + name: 20-round adapter smoke / ${{ matrix.os }} / Node 24 + needs: package + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Run isolated Native and Legacy controller for 20 rounds each + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + NND_OS_SMOKE_ROUNDS: 20 + HTTP_PROXY: '' + HTTPS_PROXY: '' + ALL_PROXY: '' + NO_PROXY: 127.0.0.1,localhost + run: >- + node --test --test-concurrency=1 + packages/network-debugger/test/e2e/os-smoke/adapter-smoke.test.mjs + + frontend: + name: Official frontend / Ubuntu / Node 24 / Native and Legacy + needs: package + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.package.outputs.artifact_name }} + path: ${{ runner.temp }}/npm-package + - name: Hydrate repository tests from the exact tarball + shell: bash + env: + NND_PACK_DIR: ${{ runner.temp }}/npm-package + run: | + set -euo pipefail + tarball="$(find "$NND_PACK_DIR" -maxdepth 1 -type f -name '*.tgz' -print -quit)" + unpacked="$RUNNER_TEMP/npm-unpacked" + mkdir -p "$unpacked" + tar -xzf "$tarball" -C "$unpacked" + rm -rf packages/network-debugger/dist + cp -R "$unpacked/package/dist" packages/network-debugger/dist + - name: Install the lockfile-pinned Chromium + run: pnpm --filter node-network-devtools exec playwright install --with-deps chromium + - run: pnpm --filter node-network-devtools test:e2e:frontend + - name: Upload frontend diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: official-frontend-node-24 + path: packages/network-debugger/test/e2e/artifacts + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 41d41e8..055b4de 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules # Testing coverage +.playwright-cli/ # Turbo .turbo @@ -43,4 +44,4 @@ yarn-error.log* # GitHub App credentials -gha-creds-*.json \ No newline at end of file +gha-creds-*.json diff --git a/README-zh_CN.md b/README-zh_CN.md index b8950e5..5910798 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -1,88 +1,102 @@ -
-

- -
Node Network Devtools

+# Node Network Devtools -

🔮 让node程序支持用chrome devtool的network选项卡调试

-

🦎 等同于浏览器的爬虫体验

-

⚙️ Powered by CDP

-

- NodeJs - Chrome - TypeScript -

+在标准 Chrome DevTools Network 面板中查看 Node.js 发出的网络请求。 -
+[English](README.md) | 简体中文 ---- +v2 提供两个互斥、完整的后端: -[English](README.md) | 简体中文 +- **Native**:直接连接 Node 实验性的 Network Inspector,不修改应用网络 API。 +- **Legacy**:通过项目自有的标准 CDP Target,捕获 HTTP/HTTPS、Fetch、可选 + Undici、WebSocket 帧和 SSE 消息。 -## 📖 介绍 +运行时只拥有调试 Target,不拥有 Chrome 进程。打开浏览器必须显式选择;端口默认 +交给操作系统分配;Legacy 应用传输已从旧的 5270 WebSocket/锁文件改为隔离的子进程 +IPC。 -如你所见,添加`--inspect`选项打开的node程序并不支持network标签,因为它不去代理用户请求。 -node network devtools正是为了解决这个问题,它一个允许您使用chrome devtools的network选项卡调试nodejs发出的请求,让debugger过程等同于浏览器中的网络爬虫体验。 +## 快速开始 -## 🎮 TODO +```bash +npm install --save-dev node-network-devtools -- [x] HTTP/HTTPS - - [x] req/res headers - - [x] payload - - [x] json str response body - - [x] binary response body - - [x] stack follow - - [x] show stack - - [x] click to jump - - [x] base - - [x] Sourcemap -- [x] WebSocket - - [x] messages - - [x] payload - - [x] headers -- [ ] Compatibility - - [x] commonjs - - [x] esmodule - - [ ] Bun -- [ ] Undici - - [ ] undici.request - - [x] undici.fetch +# 不改业务源码直接启动并打开 DevTools +npx nnd dev --open src/app.js -## 👀 预览 +# 查看当前 Node 与后端的真实能力 +npx nnd doctor --json +``` -![img](https://github.com/GrinZero/node-network-devtools/assets/70185413/5338d8f2-bb54-46fd-b243-a7a5b4af3031) +库 API: -## 📦 快速开始 +```ts +import { register } from 'node-network-devtools' -### 1. 安装 +const registration = register({ + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } +}) -```bash -# npm -npm install node-network-devtools -D -# or pnpm -pnpm add node-network-devtools -D -# or yarn -yarn add node-network-devtools -D +const ready = await registration.ready +console.log(ready.mode, ready.target, ready.capabilities, ready.fallbackReason) + +await registration.openDevtools() +await registration.dispose() ``` -### 2. Usage +为了兼容 v1,返回值仍然可以直接调用:`const unregister = register(); +unregister()`。 -只需将以下代码添加到项目的入口文件中即可。 +## 能力概览 -```typescript -import { register } from 'node-network-devtools' +| 能力 | Native | Legacy | +| ----------------------------- | ---------------------- | ------ | +| HTTP / HTTPS / Fetch 生命周期 | 取决于 Node 版本 | 支持 | +| HTTP/2 | 仅 Node 22.20+(22.x) | 不支持 | +| 响应 Body | 取决于 Node 版本 | 支持 | +| 请求 Body | 不声明支持 | 支持 | +| WebSocket 生命周期 / 帧 | 仅生命周期 | 支持 | +| SSE 消息 | 不支持 | 支持 | +| 请求/响应 Mock | 不支持 | 支持 | -process.env.NODE_ENV === 'development' && register() -``` +Native 能力根据实际 Node 版本和 Inspector 方法探测。强制 Native 时缺少能力会直接 +失败;Auto 改用 Legacy 时会返回结构化原因,不会静默降级。 -如果需要停止调试网络请求并消除副作用,只需使用 `register` 方法的返回值进行清理。 +Native HTTP/2 采用保守白名单:仅 Node 22.20+ 的 22.x 版本声明支持。Node 22.22.3 +已通过非空 h2c 生命周期实测;Node 24.16.0 与 26.8.1 在通过 `setEncoding()` 消费 +非空响应时,会触发上游实验性 Inspector 的 `Missing dataLength` 崩溃。其他及未来 +大版本在独立验证前一律报告为不支持;Legacy 也不捕获 HTTP/2。 -```typescript -import { register } from 'node-network-devtools' +## Session、HAR 与 Replay + +两个后端都可以持久化 Network Session、外置保存响应 Body、导出 HAR 1.2、关联已有 +`traceparent`,并回放请求: + +```ts +const registration = register({ + session: { directory: '.nnd/sessions/run-001', har: true } +}) + +await registration.ready +// 执行业务请求 +await registration.dispose() +``` -const unregister = register() -unregister() +```bash +npx nnd replay --dry-run --json .nnd/sessions/run-001 +npx nnd replay capture.har ``` -## ⚙️ 配置选项 +Mock 明确只属于 Legacy。Auto 配置 `legacy.mock` 时会选择 Legacy;强制 Native 会返回 +`NND_NATIVE_MOCK_CONFLICT`。 + +## 文档 + +- [npm 包完整用法](packages/network-debugger/README.md) +- [v1 到 v2 迁移说明](docs/v2-migration.md) +- [v2 架构、Plan 与验收证据](docs/v2-implementation-plan.md) -`register` 函数接受一个可选的 `RegisterOptions` 对象来定制其行为。有关可用选项及其详细说明的完整列表,请参阅[选项文档](apps/web/docs/zh/options.md)。 +发布包要求 Node.js `>=18.18`。Node 18/20 虽已 EOL,仍保留迁移兼容测试;新项目建议 +使用仍在维护的 Node 版本。`undici@^6` 是 peer dependency,确保选择 Legacy +Undici 捕获时修改的是应用使用的同一个包实例。 diff --git a/README.md b/README.md index 7c0c825..c5d4b54 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,109 @@ -
-

- -
Node Network Devtools

- -

🔮 Use chrome network devtool to debugger nodejs

-

🦎 Similar web crawler experience to browsers

-

⚙️ Powered by CDP

-

-

- NodeJs - Chrome - TypeScript -

-

- -
- ---- +# Node Network Devtools -English | [简体中文](README-zh_CN.md) - -[![node-network-devtools](https://snyk.io/advisor/npm-package/node-network-devtools/badge.svg)](https://snyk.io/advisor/npm-package/node-network-devtools) -![npm downloads](https://img.shields.io/npm/dm/node-network-devtools?label=npm%20downloads) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GrinZero/node-network-devtools) +Inspect outbound Node.js traffic in the standard Chrome DevTools Network panel. -## 📖 Introduction - -As you can see, the node program opened with the `--inspect` option does not support network tags because it does not proxy user requests. -Node network devtools is designed to address this issue by allowing you to debug requests made by nodejs using the network tab of Chrome devtools, making the debugging process equivalent to a web crawler experience in the browser. +English | [简体中文](README-zh_CN.md) -Node v22.6.0 experimentally supports network debugging. This library supports use before node v22.6.0, but the specific supported versions are unknown. +[![npm downloads](https://img.shields.io/npm/dm/node-network-devtools?label=npm%20downloads)](https://www.npmjs.com/package/node-network-devtools) -## 🎮 Features +Version 2 has two mutually exclusive backends: -- [x] HTTP/HTTPS - - [x] req/res headers - - [x] payload - - [x] json str response body - - [x] binary response body - - [x] stack follow - - [x] show stack - - [x] click to jump - - [x] base - - [x] Sourcemap -- [x] WebSocket - - [x] messages - - [x] payload - - [x] headers -- [ ] Compatibility - - [x] commonjs - - [x] esmodule -- [ ] Undici - - [ ] undici.request - - [x] undici.fetch -- [ ] Complete unit testing of oneself +- **Native** connects to Node's experimental Network Inspector without patching + application network APIs. +- **Legacy** captures HTTP/HTTPS, Fetch, opted-in Undici, WebSocket frames, and + SSE through a project-owned standard CDP target. -## 👀 Preview +The runtime owns a target, not a Chrome process. Browser opening is explicit, +ports default to OS assignment, and Legacy application transport uses isolated +child-process IPC instead of the old 5270 WebSocket/lock-file design. -![img](https://github.com/GrinZero/node-network-devtools/assets/70185413/5338d8f2-bb54-46fd-b243-a7a5b4af3031) +## Quick start -## 📦 Quick Start +```bash +npm install --save-dev node-network-devtools -### 1. Install +# Zero-code startup +npx nnd dev --open src/app.js -```bash -# npm -npm install node-network-devtools -D -# or pnpm -pnpm add node-network-devtools -D -# or yarn -yarn add node-network-devtools -D +# Diagnose the actual runtime and adapter capabilities +npx nnd doctor --json ``` -### 2. Usage - -Just add the following code to the entry file of your project. +Library usage: -```typescript +```ts import { register } from 'node-network-devtools' -process.env.NODE_ENV === 'development' && register() -``` +const registration = register({ + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } +}) -To stop debugging network requests and eliminate side effects, just use the return value of the `register` method for cleanup. +const ready = await registration.ready +console.log(ready.mode, ready.target, ready.capabilities, ready.fallbackReason) -```typescript -import { register } from 'node-network-devtools' +await registration.openDevtools() +await registration.dispose() +``` -const unregister = register() -unregister() +The handle remains callable for v1 compatibility: `const unregister = +register(); unregister()`. + +## Capability summary + +| Capability | Native | Legacy | +| ------------------------------ | ----------------------- | ------ | +| HTTP / HTTPS / Fetch lifecycle | Runtime-dependent | Yes | +| HTTP/2 | Node 22.20+ (22.x only) | No | +| Response bodies | Runtime-dependent | Yes | +| Request bodies | Not advertised | Yes | +| WebSocket lifecycle / frames | Lifecycle only | Yes | +| SSE messages | No | Yes | +| Request/response Mock | No | Yes | + +Native values are probed from the running Node version and Inspector methods. +Forced Native fails if requirements are missing; Auto returns a structured +reason whenever it uses Legacy. + +Native HTTP/2 is conservatively allowlisted only for Node 22.20+ releases in +the 22.x line. A non-empty h2c lifecycle passed on Node 22.22.3, while consuming +a non-empty response with `setEncoding()` crashes the upstream experimental +Inspector on Node 24.16.0 and 26.8.1 with `Missing dataLength`. Other and future +majors remain reported as unsupported until independently verified; Legacy does +not capture HTTP/2. + +## Session workflow + +Both backends support persistent Network sessions, external response bodies, +HAR 1.2 export, traceparent correlation, and replay: + +```ts +const registration = register({ + session: { directory: '.nnd/sessions/run-001', har: true } +}) + +await registration.ready +// run application traffic +await registration.dispose() ``` -## ⚙️ Configuration Options +```bash +npx nnd replay --dry-run --json .nnd/sessions/run-001 +npx nnd replay capture.har +``` + +Mock is intentionally Legacy-only. Auto selects Legacy when `legacy.mock` rules +are configured; forced Native reports `NND_NATIVE_MOCK_CONFLICT`. + +## Documentation -The `register` function accepts an optional `RegisterOptions` object to customize its behavior. For a complete list of available options and their detailed descriptions, please refer to the [Options Documentation](apps/web/docs/options.md). +- [npm package guide](packages/network-debugger/README.md) +- [v1 to v2 migration](docs/v2-migration.md) +- [v2 architecture and implementation evidence](docs/v2-implementation-plan.md) -![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2FGrinZero%2Fnode-network-devtools&labelColor=%237fa1f7&countColor=%23697689) \ No newline at end of file +The package requires Node.js `>=18.18`. Node 18 and 20 remain compatibility +lanes for migrations despite being EOL; maintained Node releases are preferred. +`undici@^6` remains a peer dependency so opt-in Legacy interception can patch +the application's package instance. diff --git a/apps/web/docs/get-started.md b/apps/web/docs/get-started.md index 6e4b4bb..d3d29c7 100644 --- a/apps/web/docs/get-started.md +++ b/apps/web/docs/get-started.md @@ -2,6 +2,8 @@ Node Network Devtools is a network debugging tool that integrates Chrome Devtools. It provides a network debugging experience equivalent to a browser, and is ultra easy to access. It is free of proxy competition and trouble. +Node.js `>=18.18` is required. + ## Install ::: code-tabs @@ -28,7 +30,20 @@ npm i -D node-network-devtools ## Usage -Node.js programs that support both ESM and CommonJS standards only need to introduce and call the 'register' method in the entry file. +The CLI is the recommended zero-code entry point. `--open` opens the exact +Inspector or Legacy target after it is ready; without it, the CLI prints the +target and leaves browser ownership to you. + +```bash +npx nnd dev --open src/app.js +npx nnd dev --no-wait --runner tsx src/app.ts -- --port 3000 +npx nnd doctor --json +``` + +Native/Auto starts paused by default so the frontend cannot miss startup +traffic. Add `--no-wait` when the application should start immediately. + +Library registration is also available for ESM and CommonJS applications: ::: code-tabs @@ -36,16 +51,36 @@ Node.js programs that support both ESM and CommonJS standards only need to intro ```typescript import { register } from 'node-network-devtools' -register() + +const registration = register({ + mode: 'auto', + devtools: { open: true } +}) + +const ready = await registration.ready +console.log(ready.mode, ready.target.discoveryUrl) + +// Call during application shutdown. +await registration.dispose() ``` @tab javascript ```javascript const { register } = require('node-network-devtools') -register() + +const registration = register({ + mode: 'legacy', + devtools: { open: true } +}) + +registration.ready.then(({ mode, target }) => { + console.log(mode, target.discoveryUrl) +}) ``` ::: -If you want to use options, you can go to [options](./options.md) to see the details. +Browser opening is opt-in and target ports default to an OS-assigned port. See +[configuration options](./options.md) and the +[v1 to v2 migration guide](https://github.com/GrinZero/node-network-devtools/blob/main/docs/v2-migration.md). diff --git a/apps/web/docs/options.md b/apps/web/docs/options.md index 22f9c49..2ab716e 100644 --- a/apps/web/docs/options.md +++ b/apps/web/docs/options.md @@ -1,128 +1,118 @@ -# Options +# Configuration options -## RegisterOptions +The v2 API separates backend selection, Inspector target settings, frontend +opening, Legacy hooks, and recording. -The `RegisterOptions` interface is used to configure the registration options for the network debugger. Below are detailed descriptions of each option and their default values. - -### Example - -Here is an example of using `RegisterOptions`: - -```typescript -import { RegisterOptions, register } from 'node-network-devtools' +```ts +import { register, type RegisterOptions } from 'node-network-devtools' const options: RegisterOptions = { - port: 5270, - serverPort: 5271, - autoOpenDevtool: true, - intercept: { - fetch: true, - normal: true + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false }, + session: { directory: '.nnd/sessions/local', har: true }, + legacy: { + serverPort: 0, + intercept: { normal: true, fetch: true, undici: { fetch: false } } } } -// Use options to register the network debugger -register(options) +const registration = register(options) +await registration.ready +await registration.dispose() ``` -### port - -- **Description**: Main process port -- **Default value**: `5270` - -### serverPort - -- **Description**: CDP server port for Devtool -- **Link**: [devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${serverPort}](devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${serverPort}) -- **Default value**: `5271` - -### autoOpenDevtool - -- **Description**: Whether to automatically open Devtool -- **Default value**: `true` - -### intercept - -- **Description**: Options for intercepting different types of requests. - If a property is set to `false`, that specific type of request will not be intercepted. - By default, all are intercepted if not explicitly set. - -#### intercept.fetch - -- **Description**: Whether to intercept `fetch` requests. -- **Default value**: `true` +## `mode` -#### intercept.normal +- `auto` (default): prefer a proven Native implementation and expose a + structured reason when Legacy is selected. +- `native`: require Node's experimental Network Inspector and every requested + capability; never fall back. +- `legacy`: install project capture hooks and use the project CDP target. -- **Description**: Whether to intercept `http/https` requests. -- **Default value**: `true` +## `requiredCapabilities` -#### intercept.undici +An array containing any of `http`, `https`, `fetch`, `http2`, `responseBody`, +`requestBody`, `websocketLifecycle`, `websocketFrames`, `sseMessages`, or +`initiator`. Selection fails or falls back if a backend does not provide every +required value. -- **Description**: Options for intercepting `undici` requests. Set to `false` to disable all `undici` interception. Otherwise, configure specific `undici` interception options. -- **Default value**: `false` -- **Options**: - - `fetch`: Whether to intercept `undici`'s `fetch` requests. Defaults to `false`. - - `normal`: Whether to intercept `undici`'s normal requests. Defaults to `false`. +## `inspector` -## ConnectOptions +- `host`: Inspector bind host; defaults to `127.0.0.1`. +- `port`: Inspector port; defaults to `0` for OS assignment. -The `ConnectOptions` interface configures options for connecting to the network debugger. +## `devtools` -### port +- `open`: whether library registration explicitly opens the returned target; + defaults to `false`. -- **Description**: Main process port -- **Default value**: `5270` +The backend never owns or kills the resulting browser process. -## UnregisterOptions +## `session` -The `UnregisterOptions` interface configures options for unregistering the network debugger. +- `directory`: exact output directory for `manifest.json`, `events.ndjson`, and + `bodies/`. It must not already contain Session artifacts. +- `bodyCommandTimeoutMs`: optional positive timeout for each + `Network.getResponseBody` command. +- `har`: `true` writes `session.har` in the Session directory during disposal; + a string writes to that path; false/omitted disables automatic export. -### port +The recorder closes before the backend so outstanding body commands can finish. -- **Description**: Main process port -- **Default value**: `5270` +## `legacy` -## SetRequestInterceptorOptions +### `serverPort` -The `SetRequestInterceptorOptions` interface configures options for setting a request interceptor. +Legacy CDP discovery/WebSocket target port. It defaults to `0`. The application +bridge no longer uses a TCP port. -### port +### `intercept` -- **Description**: Main process port -- **Default value**: `5270` +- `normal`: intercept `http.request/get` and `https.request/get`; default true. +- `fetch`: intercept global Fetch; default true. +- `undici.fetch`: opt into interception of separately installed Undici Fetch; + default false. `undici@^6` is a package peer so this hook observes the + application's module instance; install it explicitly when peer auto-install + is disabled. -### request +Disabled transports are reported as unavailable capabilities for that Legacy +session. -- **Description**: A function to intercept and modify outgoing requests. +### `mock` -## SetResponseInterceptorOptions +An ordered array of Legacy-only request/response rules: -The `SetResponseInterceptorOptions` interface configures options for setting a response interceptor. - -### port - -- **Description**: Main process port -- **Default value**: `5270` - -### response - -- **Description**: A function to intercept and modify incoming responses. - -## RemoveRequestInterceptorOptions - -The `RemoveRequestInterceptorOptions` interface configures options for removing a request interceptor. - -### port - -- **Description**: Main process port -- **Default value**: `5270` +```ts +{ + id: 'fixture', + match: { + url: 'https://api.example.test/*', // exact or `*` glob + method: 'POST', + headers: { 'x-test-mode': 'mock' } + }, + response: { + status: 201, + statusText: 'Created', + headers: { 'content-type': 'application/json' }, + body: '{"mocked":true}', + // bodyBase64: 'AAEC/w==', // binary alternative + delayMs: 10 + } +} +``` -## RemoveResponseInterceptorOptions +Auto selects Legacy when rules exist. Forced Native fails synchronously with +`NND_NATIVE_MOCK_CONFLICT`. -The `RemoveResponseInterceptorOptions` interface configures options for removing a response interceptor. +## Deprecated v1 fields -### port +- `adapter` → `mode` +- `requiredFeatures` → `requiredCapabilities` +- `autoOpenDevtool` → `devtools.open` +- top-level `serverPort` → `legacy.serverPort` +- top-level `intercept` → `legacy.intercept` +- `port` → remove; child IPC replaced the old 5270 WebSocket bridge -- **Description**: Main process port -- **Default value**: `5270` \ No newline at end of file +Compatibility fields still work with diagnostics during the v2 migration. diff --git a/apps/web/docs/zh/get-started.md b/apps/web/docs/zh/get-started.md index 94a95ce..3212fce 100644 --- a/apps/web/docs/zh/get-started.md +++ b/apps/web/docs/zh/get-started.md @@ -2,6 +2,8 @@ Node Network Devtools 是一款软集成了 Chrome Devtools 的网络调试工具。它提供了相当于浏览器的网络调试体验,并且非常容易接入。 +要求 Node.js `>=18.18`。 + ## 安装 ::: code-tabs @@ -28,7 +30,19 @@ npm i -D node-network-devtools ## 使用 -支持 esm 和 commonjs 标准的nodejs程序,只需要在入口文件中引入并调用`register`方法即可。 +推荐使用 CLI 零代码接入。`--open` 会在 Inspector 或 Legacy target 就绪后打开 +准确的调试地址;不传该参数时,CLI 只输出 target,浏览器进程仍由你管理。 + +```bash +npx nnd dev --open src/app.js +npx nnd dev --no-wait --runner tsx src/app.ts -- --port 3000 +npx nnd doctor --json +``` + +Native/Auto 默认会先暂停应用,避免 DevTools 前端错过启动阶段的请求;希望应用立即 +运行时可添加 `--no-wait`。 + +ESM 和 CommonJS 应用也可以使用库 API: ::: code-tabs @@ -36,16 +50,36 @@ npm i -D node-network-devtools ```typescript import { register } from 'node-network-devtools' -register() + +const registration = register({ + mode: 'auto', + devtools: { open: true } +}) + +const ready = await registration.ready +console.log(ready.mode, ready.target.discoveryUrl) + +// 在应用退出流程中调用。 +await registration.dispose() ``` @tab javascript ```javascript const { register } = require('node-network-devtools') -register() + +const registration = register({ + mode: 'legacy', + devtools: { open: true } +}) + +registration.ready.then(({ mode, target }) => { + console.log(mode, target.discoveryUrl) +}) ``` ::: -如果需要使用选项,可以前往 [选项](./options.md) 查看详细说明。 +浏览器打开行为需要显式启用,target 端口默认由操作系统分配。完整配置见 +[选项](./options.md) 和 +[v1 到 v2 迁移指南](https://github.com/GrinZero/node-network-devtools/blob/main/docs/v2-migration.md)。 diff --git a/apps/web/docs/zh/options.md b/apps/web/docs/zh/options.md index 7095a23..75bb8bc 100644 --- a/apps/web/docs/zh/options.md +++ b/apps/web/docs/zh/options.md @@ -1,137 +1,110 @@ -# 选项 +# 配置选项 -## RegisterOptions +v2 API 将后端选择、Inspector target、前端打开方式、Legacy hook 和会话记录分开配置。 -`RegisterOptions` 接口用于配置网络调试器的注册选项。以下是各个选项的详细说明及其默认值。 - -### 示例 - -以下是一个使用 `RegisterOptions` 的示例: - -```typescript -import { RegisterOptions, register } from 'node-network-devtools' +```ts +import { register, type RegisterOptions } from 'node-network-devtools' const options: RegisterOptions = { - port: 5270, - serverPort: 5271, - autoOpenDevtool: true, - intercept: { - fetch: true, - normal: true + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false }, + session: { directory: '.nnd/sessions/local', har: true }, + legacy: { + serverPort: 0, + intercept: { normal: true, fetch: true, undici: { fetch: false } } } } -// 使用 options 进行网络调试器的注册 -register(options) +const registration = register(options) +await registration.ready +await registration.dispose() ``` -### port - -- **描述**: 主进程端口 -- **默认值**: `5270` - -### serverPort - -- **描述**: CDP 服务器端口,用于 Devtool -- **链接**: [devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${serverPort}](devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${serverPort}) -- **默认值**: `5271` - -### autoOpenDevtool - -- **描述**: 是否自动打开 Devtool -- **默认值**: `true` - -### intercept - -- **描述**: 用于拦截不同类型请求的选项。 - 如果某个属性设置为 `false`,则不会拦截该特定类型的请求。 - 默认情况下,如果未明确设置,则全部拦截。 - -#### intercept.fetch - -- **描述**: 是否拦截 `fetch` 请求。 -- **默认值**: `true` - -#### intercept.normal - -- **描述**: 是否拦截 `http/https` 请求。 -- **默认值**: `true` +## `mode` -#### intercept.undici +- `auto`(默认):优先选择经过验证的 Native 实现;切换到 Legacy 时会返回结构化原因。 +- `native`:要求 Node 的实验性 Network Inspector 和所有指定能力,绝不静默回退。 +- `legacy`:安装项目自己的捕获 hook,并使用项目提供的标准 CDP target。 -- **描述**: `undici` 请求的拦截选项。设置为 `false` 以禁用所有 `undici` 拦截。否则,请配置特定的 `undici` 拦截选项。 -- **默认值**: `false` -- **选项**: - - `fetch`: 是否拦截 `undici` 的 `fetch` 请求。默认为 `false`。 - - `normal`: 是否拦截 `undici` 的普通请求。默认为 `false`。 +## `requiredCapabilities` -## ConnectOptions +数组成员可以是 `http`、`https`、`fetch`、`http2`、`responseBody`、 +`requestBody`、`websocketLifecycle`、`websocketFrames`、`sseMessages` 或 +`initiator`。如果后端缺少任一必需能力,选择过程会失败或回退。 -`ConnectOptions` 接口配置连接到网络调试器的选项。 +## `inspector` -### port +- `host`:Inspector 绑定地址,默认为 `127.0.0.1`。 +- `port`:Inspector 端口,默认为 `0`,即交给操作系统分配。 -- **描述**: 主进程端口 -- **默认值**: `5270` +## `devtools` -## UnregisterOptions +- `open`:库注册完成后是否显式打开返回的 target,默认为 `false`。 -`UnregisterOptions` 接口配置取消注册网络调试器的选项。 +后端不会持有或结束由此打开的浏览器进程。 -### port +## `session` -- **描述**: 主进程端口 -- **默认值**: `5270` +- `directory`:`manifest.json`、`events.ndjson` 和 `bodies/` 的准确输出目录; + 该目录不能已经包含 Session 产物。 +- `bodyCommandTimeoutMs`:每次 `Network.getResponseBody` 命令的可选正数超时。 +- `har`:`true` 表示在释放时将 `session.har` 写入 Session 目录;字符串表示 + 指定路径;省略或设为 `false` 则不自动导出。 -## SendMessageOptions +记录器会先于后端关闭,以便未完成的 body 命令能够正常结束。 -`SendMessageOptions` 接口配置发送消息的选项。 +## `legacy` -### port +### `serverPort` -- **描述**: 主进程端口 -- **默认值**: `5270` +Legacy CDP discovery/WebSocket target 的端口,默认为 `0`。应用与桥接子进程之间 +已经改用 IPC,不再占用旧的 TCP 端口。 -## SetRequestInterceptorOptions +### `intercept` -`SetRequestInterceptorOptions` 接口配置设置请求拦截器的选项。 +- `normal`:捕获 `http.request/get` 和 `https.request/get`,默认开启。 +- `fetch`:捕获全局 Fetch,默认开启。 +- `undici.fetch`:捕获单独安装的 Undici Fetch,默认关闭,需要显式选择。 + `undici@^6` 是 package peer,确保 hook 使用应用的同一个模块实例;关闭 peer + 自动安装时需要手动安装。 -### port +关闭的传输会在该 Legacy 会话中报告为不可用能力。 -- **描述**: 主进程端口 -- **默认值**: `5270` +### `mock` -### request +按顺序匹配的 Legacy 专用请求/响应规则: -- **描述**: 一个用于拦截和修改传出请求的函数。 - -## SetResponseInterceptorOptions - -`SetResponseInterceptorOptions` 接口配置设置响应拦截器的选项。 - -### port - -- **描述**: 主进程端口 -- **默认值**: `5270` - -### response - -- **描述**: 一个用于拦截和修改传入响应的函数。 - -## RemoveRequestInterceptorOptions - -`RemoveRequestInterceptorOptions` 接口配置移除请求拦截器的选项。 - -### port - -- **描述**: 主进程端口 -- **默认值**: `5270` +```ts +{ + id: 'fixture', + match: { + url: 'https://api.example.test/*', // 精确地址或 `*` glob + method: 'POST', + headers: { 'x-test-mode': 'mock' } + }, + response: { + status: 201, + statusText: 'Created', + headers: { 'content-type': 'application/json' }, + body: '{"mocked":true}', + // bodyBase64: 'AAEC/w==', // 二进制响应可用此字段 + delayMs: 10 + } +} +``` -## RemoveResponseInterceptorOptions +配置规则后,Auto 会选择 Legacy。强制 Native 会同步抛出 +`NND_NATIVE_MOCK_CONFLICT`。 -`RemoveResponseInterceptorOptions` 接口配置移除响应拦截器的选项。 +## 已弃用的 v1 字段 -### port +- `adapter` → `mode` +- `requiredFeatures` → `requiredCapabilities` +- `autoOpenDevtool` → `devtools.open` +- 顶层 `serverPort` → `legacy.serverPort` +- 顶层 `intercept` → `legacy.intercept` +- `port` → 删除;子进程 IPC 已取代旧的 5270 WebSocket bridge -- **描述**: 主进程端口 -- **默认值**: `5270` \ No newline at end of file +迁移到 v2 期间,这些兼容字段仍然可用,并会产生诊断信息。 diff --git a/docs/test-evidence/v2/pr-64/README.md b/docs/test-evidence/v2/pr-64/README.md new file mode 100644 index 0000000..a74f1ba --- /dev/null +++ b/docs/test-evidence/v2/pr-64/README.md @@ -0,0 +1,186 @@ +# PR #64 manual acceptance evidence + +Result: **14/14 manual test cases passed**. + +This is the human-observable acceptance layer for the automated v2 suite. The +run used the exact packed package from the tested product commit, an isolated +consumer, real loopback traffic, the public CLI/runtime APIs, standard CDP +discovery, and the official Chromium DevTools Network panel. The evidence set +contains 26 privacy-reviewed screenshots, 17 structured artifacts, five +reproduction harnesses, two test-only localhost certificate inputs, and a +checksum manifest. + +## Evidence identity + +| Field | Value | +| --------------------- | ------------------------------------------------------------------ | +| Pull request | [#64](https://github.com/GrinZero/node-network-devtools/pull/64) | +| Tested product commit | `449d47db89109e826eb0e7e0584777365eac3f9b` | +| Installed package | `node-network-devtools@2.0.0` | +| Packed files | 190 | +| Tarball SHA-256 | `e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac` | +| Harness SHA-256 | `46f81eb2930040a31f402cf4cb113758628eb72c9db27617babba1d3df31af27` | +| Primary runtime | Node.js `v24.16.0`, `darwin-arm64` | +| Compatibility probe | Node.js `v22.22.3`, `v24.16.0`, and `v26.8.1` | +| Browser | Chrome for Testing `151.0.7922.34` | +| Browser automation | `@playwright/cli 0.1.18`, engine `1.63.0-alpha-2026-08-05` | +| Test date | 2026-08-28 (Asia/Shanghai) | + +The package was built and packed at the tested product commit, hashed, and +installed into an otherwise empty consumer. The harness imports only the public +exports from that installed tarball. The package identity is repeated in the +[Native runtime artifact](../../../../output/playwright/pr-64/artifacts/native-runtime.json), +[Legacy runtime artifact](../../../../output/playwright/pr-64/artifacts/legacy-runtime.json), +and [manifest](../../../../output/playwright/pr-64/manifest.json). + +## Method and trust boundary + +- Playwright operated visible scenario controls and the official Chromium + DevTools frontend. Every click caused the isolated consumer process to issue + real outbound traffic; the screenshots are not a mock DevTools UI. +- Computer Use was used for the exploratory integrated-terminal CLI pass. The + final repeatable CLI proof was rerun through a Playwright control page and + persisted as both a screenshot and structured JSON. +- HTTP, HTTPS, Fetch, SSE, WebSocket, failed requests, mocks, and replay all used + local loopback servers. HTTPS used the retained test-only localhost + certificate; its client explicitly disabled trust verification for that + fixture only. +- The harness fetched and asserted the complete `/json/list`, `/json/version`, + and `/json/protocol` contracts, including the Network domain. Raw discovery + screenshots containing a local file URL were deliberately excluded; the + privacy-safe capability page, version page, and structured response retain + the same protocol assertions. +- Session manifests, raw CDP events, HAR files, replay summaries, CLI results, + capability-boundary assertions, and disposal probes are committed alongside + the screenshots. Only local filesystem prefixes were redacted; protocol + methods, statuses, bodies, counters, and lifecycle semantics were preserved. +- [`checksums.sha256`](../../../../output/playwright/pr-64/checksums.sha256) + covers all 51 retained files in the evidence bundle other than + `.gitignore` and the checksum file itself. Screenshots satisfy the requested + visual-evidence option; no video is committed. + +## Test cases + +| ID | Manual action | Expected result | Observed result and evidence | Status | +| ----- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| MT-01 | Run `nnd --version`, then run `nnd doctor --json` with Node network inspection enabled. | Version is `2.0.0`; doctor is healthy, has no missing requirement, and selects Native. | `version=2.0.0`, `ok=true`, `experimentalFlag=true`, `missingRequired=[]`, `selected=native`. [Screenshot](../../../../output/playwright/pr-64/screenshots/MT-01-cli-version-doctor-native.png) · [CLI JSON](../../../../output/playwright/pr-64/artifacts/cli-manual-results.json) | **PASS** | +| MT-02 | Run public `nnd dev` in Native and Legacy modes, force Native with a mock rule, and run `nnd dev --open`. | Both modes inject the preload and expose a target; invalid Native + Mock fails; `--open` launches exactly the advertised target. | Six visible CLI cases passed. Native received inspection/import flags; Legacy received the import; conflict returned `NND_NATIVE_MOCK_CONFLICT`; the OS launch was redirected to an exact-target CDP verifier and occurred once. [Screenshot](../../../../output/playwright/pr-64/screenshots/MT-02-cli-zero-code-and-conflict.png) · [CLI JSON](../../../../output/playwright/pr-64/artifacts/cli-manual-results.json) | **PASS** | +| MT-03 | Register the exact package in Native mode and inspect identity, public references, target, and capabilities. | Native is ready without fallback; public networking functions remain unpatched; only verified capabilities are advertised. | Native selected; original `fetch`, `http.request`, and `https.request` references were preserved. Node 24 correctly reports `http2=false`, `requestBody=false`, `websocketFrames=false`, and `sseMessages=false`. [Screenshot](../../../../output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png) · [Runtime JSON](../../../../output/playwright/pr-64/artifacts/native-runtime.json) | **PASS** | +| MT-04 | Fetch the Native target's `/json/list`, `/json/version`, and `/json/protocol` endpoints. | One matching Node target is discoverable; version metadata is valid; the protocol exposes the Network domain. | Target count `1`, matching id, browser `node.js/v24.16.0`, protocol `1.1`, Network domain present with 6 commands and 8 events. [Capability/discovery screenshot](../../../../output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png) · [Version screenshot](../../../../output/playwright/pr-64/screenshots/MT-04-native-version.png) · [Structured contract/runtime JSON](../../../../output/playwright/pr-64/artifacts/native-runtime.json) | **PASS** | +| MT-05 | Open official DevTools and trigger real HTTP GET, HTTPS GET, and Fetch POST requests; inspect list, headers, and response. | Each request appears once with correct status and metadata; response bodies are readable. | Requests returned `200`, `200`, and `201`; the Fetch headers carry the correlation token and its response echoes the manual value. Native does not claim request-body retrieval. [List](../../../../output/playwright/pr-64/screenshots/MT-05-native-devtools-network-list.png) · [Headers](../../../../output/playwright/pr-64/screenshots/MT-05-native-fetch-headers.png) · [Response](../../../../output/playwright/pr-64/screenshots/MT-05-native-fetch-response.png) · [Events](../../../../output/playwright/pr-64/artifacts/native-events.ndjson) | **PASS** | +| MT-06 | Reload official Native DevTools, reconnect to the same target, and trigger another GET. | The frontend reconnects and captures post-reload traffic without replaying old rows as new requests. | A new `native-http-get-08` request appears with status `200` after reload/reconnect. [Screenshot](../../../../output/playwright/pr-64/screenshots/MT-06-native-devtools-reload-reconnect.png) · [Session manifest](../../../../output/playwright/pr-64/artifacts/native-session-manifest.json) | **PASS** | +| MT-07 | Register in Auto mode with Legacy mock rules and inspect backend selection and capabilities. | Auto selects Legacy with an explicit reason and exposes its larger detail-level capability set. | Backend `legacy`; reason `NND_AUTO_LEGACY_MOCK_REQUIRED`; request-body, WebSocket-frame, and SSE-message capture enabled. [Screenshot](../../../../output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png) · [Runtime JSON](../../../../output/playwright/pr-64/artifacts/legacy-runtime.json) | **PASS** | +| MT-08 | Fetch the Legacy target's `/json/list`, `/json/version`, and `/json/protocol` endpoints. | One matching project-owned CDP target is discoverable on loopback and the protocol exposes Network. | Target count `1`, matching id, browser `node-network-devtools/2`, protocol `1.3`, Network domain present with 7 commands and 12 events. [Capability/discovery screenshot](../../../../output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png) · [Version screenshot](../../../../output/playwright/pr-64/screenshots/MT-08-legacy-version.png) · [Structured contract/runtime JSON](../../../../output/playwright/pr-64/artifacts/legacy-runtime.json) | **PASS** | +| MT-09 | In official DevTools, trigger Legacy HTTP GET, HTTPS GET, and Fetch POST; inspect list, Payload, and Response. | Statuses are correct and both POST request body and response body are available. | Fetch is `201`; Payload contains `manual-request-body:legacy-fetch-post-03`; Response contains the matching echo. [List](../../../../output/playwright/pr-64/screenshots/MT-09-legacy-devtools-network-list.png) · [Payload](../../../../output/playwright/pr-64/screenshots/MT-09-legacy-fetch-payload.png) · [Response](../../../../output/playwright/pr-64/screenshots/MT-09-legacy-fetch-response.png) · [Events](../../../../output/playwright/pr-64/artifacts/legacy-events.ndjson) | **PASS** | +| MT-10 | Trigger matching Legacy mock rules through `http.request` and Fetch; inspect status, body, headers, and origin counter. | HTTP returns `207`, Fetch returns `202`, and neither request reaches the origin server. | Both responses traverse the normal Network path; mock bodies/headers are visible and `originLeakCount=0`. [HTTP](../../../../output/playwright/pr-64/screenshots/MT-10-legacy-mock-http-response.png) · [Fetch](../../../../output/playwright/pr-64/screenshots/MT-10-legacy-mock-fetch-response.png) · [Summary](../../../../output/playwright/pr-64/artifacts/legacy-finalize-summary.json) | **PASS** | +| MT-11 | Trigger SSE and WebSocket exchanges on both backends and inspect the visible detail tabs and protocol counters. | Native reports only what upstream provides; Legacy exposes SSE messages and sent/received WebSocket frames. | Native: one create/close pair, zero frames, SSE request visible with zero message events. Legacy: two SSE messages plus 2 sent and 2 received frames. [Native lifecycle](../../../../output/playwright/pr-64/screenshots/MT-11-native-websocket-lifecycle.png) · [Legacy SSE](../../../../output/playwright/pr-64/screenshots/MT-11-legacy-sse-eventstream.png) · [Legacy WebSocket](../../../../output/playwright/pr-64/screenshots/MT-11-legacy-websocket-messages.png) · [Native assertion](../../../../output/playwright/pr-64/artifacts/native-finalize-summary.json) · [Legacy assertion](../../../../output/playwright/pr-64/artifacts/legacy-finalize-summary.json) | **PASS** | +| MT-12 | Finalize both recordings; inspect Session, HAR, trace correlation, library replay, and public CLI replay in dry/real modes. | Manifests complete without body errors; HAR is coherent; explicit trace context survives; dry and real replay both pass. | Native replay `5/5 + 5/5`; Legacy replay `6/6 + 6/6`; public CLI replay `2/2 + 2/2` and preserved the POST body; trace headers were preserved only on the explicitly traced request. [Native](../../../../output/playwright/pr-64/screenshots/MT-12-native-session-har-replay-trace.png) · [Legacy](../../../../output/playwright/pr-64/screenshots/MT-12-legacy-session-har-replay-trace.png) · [CLI](../../../../output/playwright/pr-64/artifacts/cli-manual-results.json) · [Native summary](../../../../output/playwright/pr-64/artifacts/native-finalize-summary.json) · [Legacy summary](../../../../output/playwright/pr-64/artifacts/legacy-finalize-summary.json) | **PASS** | +| MT-13 | Trigger a connection-reset failure on both backends and probe Native HTTP/2 on Node 22, 24, and 26. | Failed requests emit exactly one terminal failure; HTTP/2 is advertised only where its complete lifecycle is verified. | Both backends emitted one `requestWillBeSent` and one `loadingFailed`, with no response/finished event. Node 22.22.3 completed HTTP/2 with body `h2-ok`; Node 24.16/26.8 reproduced the upstream Inspector `Missing dataLength` crash and correctly advertised `http2=false`. [Native failure](../../../../output/playwright/pr-64/screenshots/MT-13-native-failed-request.png) · [Legacy failure](../../../../output/playwright/pr-64/screenshots/MT-13-legacy-failed-request.png) · [Native boundary](../../../../output/playwright/pr-64/screenshots/MT-13-native-boundary-assertions.png) · [Legacy boundary](../../../../output/playwright/pr-64/screenshots/MT-13-legacy-boundary-assertions.png) · [HTTP/2 probe](../../../../output/playwright/pr-64/artifacts/native-http2-probe.json) | **PASS** | +| MT-14 | Dispose each registration, then probe discovery HTTP and the target WebSocket. | State becomes disposed and both endpoints reject new connections. | Both summaries report `registrationState=disposed`, `discoveryClosed=true`, and `targetSocketClosed=true`. [Native](../../../../output/playwright/pr-64/screenshots/MT-14-native-dispose-cleanup.png) · [Legacy](../../../../output/playwright/pr-64/screenshots/MT-14-legacy-dispose-cleanup.png) · [Native JSON](../../../../output/playwright/pr-64/artifacts/native-dispose-summary.json) · [Legacy JSON](../../../../output/playwright/pr-64/artifacts/legacy-dispose-summary.json) | **PASS** | + +## Representative visual evidence + +
+CLI, runtime selection, and discovery + +![CLI version and doctor](../../../../output/playwright/pr-64/screenshots/MT-01-cli-version-doctor-native.png) + +![Six exact-package CLI cases](../../../../output/playwright/pr-64/screenshots/MT-02-cli-zero-code-and-conflict.png) + +![Native runtime identity and capabilities](../../../../output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png) + +![Legacy runtime identity and capabilities](../../../../output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png) + +
+ +
+Official DevTools request details + +![Native DevTools Network list](../../../../output/playwright/pr-64/screenshots/MT-05-native-devtools-network-list.png) + +![Native Fetch response](../../../../output/playwright/pr-64/screenshots/MT-05-native-fetch-response.png) + +![Legacy Fetch payload](../../../../output/playwright/pr-64/screenshots/MT-09-legacy-fetch-payload.png) + +![Legacy Fetch response](../../../../output/playwright/pr-64/screenshots/MT-09-legacy-fetch-response.png) + +![Legacy mock HTTP response](../../../../output/playwright/pr-64/screenshots/MT-10-legacy-mock-http-response.png) + +![Legacy SSE messages](../../../../output/playwright/pr-64/screenshots/MT-11-legacy-sse-eventstream.png) + +![Legacy WebSocket messages](../../../../output/playwright/pr-64/screenshots/MT-11-legacy-websocket-messages.png) + +
+ +
+Recording, boundaries, failure, and cleanup + +![Native Session, HAR, Replay, and Trace](../../../../output/playwright/pr-64/screenshots/MT-12-native-session-har-replay-trace.png) + +![Legacy Session, HAR, Replay, and Trace](../../../../output/playwright/pr-64/screenshots/MT-12-legacy-session-har-replay-trace.png) + +![Native capability boundary assertions](../../../../output/playwright/pr-64/screenshots/MT-13-native-boundary-assertions.png) + +![Legacy failed-request lifecycle](../../../../output/playwright/pr-64/screenshots/MT-13-legacy-failed-request.png) + +![Native disposal and closed endpoints](../../../../output/playwright/pr-64/screenshots/MT-14-native-dispose-cleanup.png) + +![Legacy disposal and closed endpoints](../../../../output/playwright/pr-64/screenshots/MT-14-legacy-dispose-cleanup.png) + +
+ +## Structured evidence summary + +| Backend | Requests | Events | Bodies | Failed/body errors | HAR statuses | Replay dry/real | Dispose | +| ------- | -------: | -----: | -----: | -----------------: | --------------------------------------------- | --------------- | ------------------------- | +| Native | 9 | 24 | 6 | 1 / 0 | `200,200,201,200,0,200,0,0,200` | 5/5, 5/5 | discovery + socket closed | +| Legacy | 12 | 54 | 10 | 1 / 0 | `200,200,200,200,200,201,200,0,200,0,207,202` | 6/6, 6/6 | discovery + socket closed | + +The zero-status HAR entries are intentionally incomplete SSE, WebSocket, or +failed-request lifecycles. Their exact terminal-event assertions are recorded in +the finalize summaries. There were no response-body retrieval errors and no +mock-origin leaks. + +The HTTP/2 compatibility probe deserves separate emphasis. A non-empty h2c +response completed `requestWillBeSent -> responseReceived -> loadingFinished` +on Node 22.22.3. The same exact package and request reproduced an upstream +`node:internal/inspector/network_http2` crash on Node 24.16.0 and 26.8.1. +Consequently, commit `449d47d` changed the public capability matrix from an +open-ended minimum-version claim to the verified Node 22.20+ 22.x line. This is +consistent with Node's still-open +[network-inspection stabilization tracker](https://github.com/nodejs/node/issues/53946) +and avoids promising behavior that the runtime cannot safely deliver. + +## Reproduction + +The retained harnesses are +[`manual-evidence-server.mjs`](../../../../output/playwright/pr-64/manual-evidence-server.mjs), +[`manual-cli-evidence-server.mjs`](../../../../output/playwright/pr-64/manual-cli-evidence-server.mjs), +[`manual-cli-case.sh`](../../../../output/playwright/pr-64/manual-cli-case.sh), +[`manual-native-mock-conflict.mjs`](../../../../output/playwright/pr-64/manual-native-mock-conflict.mjs), +and +[`manual-native-http2-probe.mjs`](../../../../output/playwright/pr-64/manual-native-http2-probe.mjs). + +To reproduce: + +1. Check out product commit `449d47db89109e826eb0e7e0584777365eac3f9b`, + install from the lockfile, build, and pack `packages/network-debugger`. +2. Verify the tarball SHA-256 above, then install it into an empty consumer. +3. Set `NND_MANUAL_CONSUMER_ROOT`, `NND_MANUAL_PRODUCT_COMMIT`, and + `NND_MANUAL_TARBALL_SHA256`. Start the Native harness with + `--experimental-network-inspection`, or start the Legacy harness normally. +4. Open the printed control URL. Click one scenario at a time and inspect the + printed official DevTools URL. +5. Run the CLI evidence server's six visible cases and the three-version HTTP/2 + probe. Compare all retained files with `checksums.sha256`. + +## Scope + +- All business traffic was loopback-only. Public-network and production + endpoints were intentionally out of scope. +- Native limitations are visible results, not simulated features. On the + primary Node 24 run, request bodies, WebSocket frames, SSE messages, and + HTTP/2 are not advertised. Those detail-level claims are tested on Legacy or + on the verified Node 22 HTTP/2 runtime. +- This manual run covers macOS arm64. The PR quality workflow remains responsible + for the repeatable Linux, macOS, Windows, and supported-Node automation. diff --git a/docs/v2-implementation-plan.md b/docs/v2-implementation-plan.md new file mode 100644 index 0000000..0605822 --- /dev/null +++ b/docs/v2-implementation-plan.md @@ -0,0 +1,429 @@ +# Node Network Devtools v2 implementation plan + +Status: complete + +Started: 2026-08-28 + +Owner: repository maintainers and Codex implementation session + +This document is the authoritative implementation and completion plan for the v2 +architecture. A phase is complete only when its code, tests, documentation, and +acceptance evidence are all present in the current worktree. A passing subset of +tests is not sufficient to mark the overall plan complete. + +## Goals + +1. Make `NodeNativeAdapter` a first-class backend from the first implementation + phase. +2. Replace mocked "CDP correctness" tests with real protocol end-to-end tests. +3. Separate network capture, CDP target creation, and frontend launching. +4. Stop coupling the debug server lifecycle to a Chrome process. +5. Preserve a Legacy backend for capabilities missing from Node's native network + inspector. +6. Improve zero-code setup, diagnostics, configuration, watch-mode behavior, and + package-consumer verification. +7. Add session recording, HAR export, replay, Legacy-only mocking, and trace + correlation after the runtime and connection layers are stable. + +## Explicit non-goals for this plan + +- Security hardening as a dedicated workstream. +- Incoming HTTP/server request inspection. +- A custom DevTools frontend. +- Bun or Deno support. +- Native/Legacy hybrid capture for a single session. +- Pretending that Node Native capabilities exist when the selected Node release + does not provide them. + +## Architectural decisions + +### One complete backend per session + +`NodeNativeAdapter` and `LegacyAdapter` are mutually exclusive complete +backends. Native uses Node's own capture, CDP implementation, and Inspector +target. Legacy uses the existing monkey-patch capture and a project-owned CDP +bridge. They must never emit the same request in one session. + +```text +register / preload / CLI + | + v + RuntimeController + |- ConfigResolver + |- AdapterSelector + `- RegistrationHandle + | + +-----+-----------+ + | | + v v +NodeNativeAdapter LegacyAdapter + | | +Node Inspector Legacy capture + bridge + | | + +--------+--------+ + v + DevtoolsTarget + | | + v v + optional frontend optional ProtocolTap/session pipeline +``` + +### Connection ownership + +The backend owns a debuggable target, not a browser. Core runtime code must not +start a Chrome remote-debugging server, send `Page.navigate`, retain a browser +process handle, or kill a browser during disposal. + +### Capability-driven selection + +Selection is based on an explicit, versioned capability matrix verified by E2E +tests. Forced Native fails if requirements are not met. Only Auto may fall back +to Legacy, and it must expose a structured fallback reason. + +## Public API target + +```ts +const registration = register({ + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } +}) + +const ready = await registration.ready +console.log(ready.mode, ready.target, ready.capabilities) + +await registration.openDevtools() +await registration.dispose() +``` + +During the compatibility period the returned handle remains callable, so the +existing `const unregister = register(); unregister()` form still works. + +Mode semantics: + +- `native`: require the experimental flag and required runtime capabilities; + never silently fall back. +- `legacy`: always use project-owned capture and CDP bridge. +- `auto`: prefer a proven Native baseline, otherwise use Legacy with a visible + fallback diagnostic. + +## Phase 1: Native backend and real Native CDP E2E + +### Deliverables + +- [x] Define runtime, adapter, target, session, diagnostic, and capability types. +- [x] Implement `AdapterSelector`. +- [x] Implement `NodeNativeAdapter` using `node:inspector`. +- [x] Reuse an existing Inspector endpoint without taking ownership of it. +- [x] Open an Inspector on an OS-assigned port when the adapter owns the target. +- [x] Read the canonical target descriptor from Node's `/json/list` endpoint. +- [x] Return a backward-compatible observable registration handle. +- [x] Ensure Native never patches `fetch`, `http.request`, or `https.request`. +- [x] Ensure Native never forks or starts the Legacy 5270/5271 services. +- [x] Add real Native protocol E2E tests against the built package. +- [x] Add a CI quality workflow that runs the first Native E2E gate. + +### Native E2E baseline + +- [x] `Network.enable` returns a response with the same command id. +- [x] A real HTTP request emits a valid lifecycle. +- [x] A real Fetch request emits a valid lifecycle. +- [x] A failed request emits `Network.loadingFailed` only. +- [x] `Network.getResponseBody` returns the actual fixture body where supported. +- [x] `Runtime.evaluate('process.pid')` proves the client is attached to the + target process. +- [x] Initiator data points to a real fixture source location where supported. +- [x] Explicit Native without the required flag fails with an actionable error. +- [x] Auto fallback returns a structured reason. +- [x] Disposal closes only an Inspector created by the adapter. + +## Phase 2: target connection and developer experience + +### Deliverables + +- [x] Add the `nnd` CLI. +- [x] Add a side-effect preload export. +- [x] Add `nnd dev`, `nnd doctor`, and `nnd doctor --json`. +- [x] Launch Native targets with the experimental network-inspection flag. +- [x] Support wait-for-first-frontend and no-wait startup modes. +- [x] Add explicit frontend launching through a separate `FrontendLauncher`. +- [x] Remove the Chrome port 9333 polling and `Page.navigate` implementation. +- [x] Stop retaining or killing a Chrome process. +- [x] Add hosted official DevTools frontend smoke tests. +- [x] Add CJS, ESM, tsx, Nest compiled, and Node watch fixtures. + +### Developer-experience acceptance + +- [x] `nnd dev app.js` starts a Native-capable target with no source edit. +- [x] `nnd dev --open app.js` explicitly opens the returned target URL. +- [x] Library usage does not open a browser by default. +- [x] Ready output contains mode, target, capabilities, and fallback reason. +- [x] Diagnostics use stable codes and provide actionable hints. +- [x] Repeated registration is idempotent for equal configuration and rejects + conflicting configuration. + +## Phase 3: Legacy adapter and real Legacy CDP E2E + +### Deliverables + +- [x] Move current capture behavior behind `LegacyAdapter`. +- [x] Complete Auto/Native/Legacy selection and old-option migration. +- [x] Mark current request hooks as Legacy-only. +- [x] Add real Legacy protocol E2E with no mocked server or manufactured CDP + events. +- [x] Respond to all command ids with a result or a standard CDP error. +- [x] Implement correct `Network.loadingFailed` behavior. +- [x] Preserve HTTP, Fetch, binary, SSE, WebSocket, and initiator behavior. + +### Legacy protocol baseline + +- [x] HTTP GET and Fetch POST. +- [x] Text, gzip, and binary response bodies. +- [x] Redirect, abort, reset, and timeout behavior. +- [x] SSE event name, id, data, and ordering. +- [x] WebSocket handshake, text frame, binary frame, and close. +- [x] Concurrent requests use stable, distinct request ids. +- [x] CJS and ESM package consumers. + +## Phase 4: Legacy transport and discovery + +### Deliverables + +- [x] Replace the application-to-fork 5270 WebSocket with child-process IPC. +- [x] Remove the lock-file and WebSocket health-ping mechanisms. +- [x] Use a single HTTP server for Legacy target discovery and CDP WebSocket + upgrade. +- [x] Implement `/json/list`, `/json/version`, and `/json/protocol`. +- [x] Default to port `0`; never probe a free port before binding. +- [x] Support multiple clients and DevTools refresh/reconnect. +- [x] Ensure abnormal bridge exits produce bounded recovery and visible status. +- [x] Ensure disposal leaves no port, timer, child, or pending promise behind. + +## Phase 5: compatibility and release gates + +### Deliverables + +- [x] Maintain a capability matrix backed by tests rather than runtime skipping. +- [x] Add Node 18/20/22/24/26 runtime coverage as appropriate per adapter. +- [x] Run mandatory Ubuntu protocol E2E on pull requests. +- [x] Run Windows and macOS adapter smoke tests. +- [x] Run a complete OS/runtime matrix nightly. +- [x] Build and `npm pack` once, then test the published artifact as CJS and ESM + consumers. +- [x] Make npm publishing depend on the same reusable quality workflow. +- [x] Document Native/Legacy differences and migration from old options. + +## Phase 6: session and debugging enhancements + +### Deliverables + +- [x] Add a common protocol event journal/tap for both backends. +- [x] Store sessions as `manifest.json`, `events.ndjson`, and external body files. +- [x] Export valid HAR with matching text and binary bodies. +- [x] Replay requests from a session or HAR, including dry-run mode. +- [x] Implement request/response mocking for Legacy only. +- [x] Reject Native plus Mock as an explicit capability conflict. +- [x] Correlate existing `traceparent` values without injecting tracing by + default. +- [x] Add real-network E2E for Session, HAR, Replay, Mock, and Trace. + +## E2E architecture + +Protocol E2E uses a thin raw WebSocket CDP client for both backends. It must +spawn a real packaged consumer, connect to the real endpoint, trigger a real +loopback request, and assert observed protocol invariants. It must not import a +backend plugin directly. + +The fixture controller communicates with target applications over process IPC so +test-control traffic does not pollute Network events. Each scenario uses a unique +URL/query token and waits on explicit events instead of fixed sleeps. + +Frontend smoke tests use the official DevTools frontend bundled with the exact +Playwright Chromium revision pinned in the lockfile. Chromium's loopback +remote-debugging HTTP server serves those generated assets locally. The tests +verify frontend connection, Network model population, response body retrieval, +reconnect, console errors, page errors, and the absence of non-loopback +frontend traffic. The source-only `chrome-devtools-frontend` npm tarball is not +treated as a runnable frontend build. + +Required failure artifacts: + +- Complete CDP inbound/outbound NDJSON journal. +- Target stdout and stderr. +- Session descriptor and capability selection. +- Playwright trace, screenshot, console, and page errors for frontend tests. + +## CI target matrix + +Pull-request minimum: + +| Job | Runtime and platform | +| ---------------- | ------------------------------------------ | +| Unit | Node 20/22/24/26 on Ubuntu | +| Legacy runtime | Node 18/20/22/24/26 on Ubuntu | +| Native runtime | supported Node 22/24/26 releases on Ubuntu | +| Adapter OS smoke | Node 24 on Windows and macOS | +| Frontend smoke | Node 24 on Ubuntu, Native and Legacy | +| Pack consumer | CJS and ESM from the generated tarball | + +The runtime E2E controller should use `node:test` so evidence for Node 18 does not +depend on the Vitest controller's own minimum runtime. + +## Suggested source layout + +```text +packages/network-debugger/ + src/ + runtime/ + adapters/ + node-native/ + legacy/ + target/ + diagnostics/ + config/ + session/ + preload/ + cli/ + legacy-bridge/ + test/e2e/ + fixtures/ + harness/ + contracts/ + protocol/ + frontend/ +``` + +## Final completion audit + +Before declaring the overall plan complete, inspect current evidence for every +checkbox above and additionally prove: + +- [x] Native requests appear exactly once and never pass through Legacy code. +- [x] Native does not change the references of supported network APIs. +- [x] Legacy retains every previously documented capability. +- [x] Both backends connect through actual standard targets. +- [x] The project no longer owns a Chrome process. +- [x] Protocol E2E contains no `vi.mock`, fake WebSocket server, or hand-written + `Network.*` event used as product evidence. +- [x] Both protocol suites pass 50 consecutive runs without a failure or leaked + process. +- [x] Windows and macOS smoke suites pass 20 consecutive runs. +- [x] Frontend smoke passes 10 consecutive runs. +- [x] Pull requests and publishing are blocked by the verified quality workflow. +- [x] Documentation describes actual current capabilities, not intended ones. +- [x] Every planned artifact exists in the packed npm output when required. + +## Progress log + +- 2026-08-28: Plan established from repository inspection, Node official + network-inspection capabilities, and a successful local Node 24.16 Native CDP + probe. Phase 1 started. +- 2026-08-28: Phase 1 requirement audit passed. Evidence: 886 unit tests; clean + Vite build plus declaration emit; four real built-package Native CDP E2E + scenarios; ten consecutive E2E repetitions; owned/reused Inspector lifecycle + tests; public forced-Native and Auto-fallback tests; and a pull-request quality + workflow running unit, build, and Native E2E. Node 24.16 emits Native + `wallTime` in epoch milliseconds, so the Native-only E2E records and validates + that upstream deviation without transforming the direct Inspector protocol. +- 2026-08-28: Phase 2 requirement audit passed. Evidence: 910 unit tests; clean + declaration and dual-runtime builds including ESM-only preload; four real + Native protocol scenarios; eight built-package CLI E2E scenarios covering + doctor, real `--open`/Inspector-wait resume, Auto-to-Legacy fallback, CJS, + ESM, tsx, watch, compiled Nest-style startup, signals, and orphan cleanup; + and an official pinned Chromium DevTools frontend smoke that populated its + real Network model, retrieved two response bodies across a reload/reconnect, + rejected non-loopback frontend traffic, and passed ten consecutive runs. The + CI quality job installs that pinned browser and runs all Phase 1/2 gates. +- 2026-08-28: Phase 3 requirement audit passed. Legacy capture is isolated behind + its adapter and uses a real built-package CDP target. Evidence: eight protocol + scenarios pass for both CJS and ESM consumers and ten consecutive repetitions + completed 80/80; coverage includes HTTP, Fetch POST (including multi-chunk + request bodies), text/gzip/binary bodies, redirect/reset/timeout/abort, SSE, + WebSocket text/binary/close, 20 concurrent command ids, response-body retrieval, + standard CDP errors, and initiators. The official frontend also passed ten + consecutive Legacy runs and a combined Native/Legacy reconnect run. +- 2026-08-28: Phase 4 requirement audit passed. The Legacy application bridge now + uses advanced-serialization child IPC and a single loopback HTTP/WebSocket target + on an OS-assigned port; the old 5270 transport, lock file, and health endpoint are + gone. Fifty-five target/discovery tests and 25 IPC lifecycle tests cover bounded + queues/history, multi-client isolation, stable target recovery, child flag + sanitization, and terminal diagnostics. A CLI shutdown race found by full E2E was + fixed by making the dedicated bridge close and exit on parent IPC disconnect; + the focused regression and the complete 8/8 CLI suite leave no matching child. +- 2026-08-28: Phase 6 requirement audit passed. A backend-neutral real CDP + `ProtocolTap` records atomic manifests, NDJSON events, integrity-indexed external + bodies, and existing trace context; HAR 1.2 export preserves text/binary bodies, + while library and `nnd replay` APIs support Session/HAR dry-run and real replay. + Legacy-only HTTP/Fetch/Undici mocks traverse normal capture, Auto exposes a + structured reason, and forced Native fails with `NND_NATIVE_MOCK_CONFLICT`. + Twenty-two Session unit/integration tests and the full unit suite passed. The + built-package enhancement E2E passed 2/2 and then 20/20 across ten repetitions, + covering six real business requests/bodies, HAR, Replay, HTTP+Fetch Mock, + traceparent/tracestate, and zero origin leakage. That gate exposed and verified + the fix for an internal ProtocolTap self-observation recursion: the final + manifest contains exactly six requests, six bodies, zero failures, and no child + process leak. +- 2026-08-28: Phase 5 requirement audit passed. The reusable quality workflow + builds and packs exactly once, then gates isolated CJS/ESM consumers, Ubuntu + Native/Legacy/CLI/enhancement protocol suites, the official frontend, Node + 20/22/24/26 unit lanes, Legacy Node 18/20/22/24/26, Native Node 22/24/26, and + Windows/macOS Node 24 20-round adapter smoke. Nightly adds Legacy on both OSes + for all five runtimes and Native for 22/24/26. Local exact-tarball evidence was + green across those available runtimes and macOS smoke; Node 22's incomplete + Native Fetch body caused the public cross-transport `responseBody` capability + to be conservatively disabled there. Publishing consumes the same SHA-256 + verified artifact through the reusable gate with OIDC/provenance and checks + its release tag. That phase-5 candidate `node-network-devtools-2.0.0.tgz` installed in CJS + and ESM consumers, exposed all five exports/two bins, contained 187 files, and + passed `npm publish --dry-run` without performing a real publish. +- 2026-08-28: Final local audit passed on the post-review tree. Three independent + reviews found and drove fixes for the Node `--import` minimum (`>=18.18`), + side-effect preload declarations, stale published-site instructions, missing + packed LICENSE, finite Legacy/`-e`/`-p` process lifetime, and negotiated + `permessage-deflate` capture. The final local gate passed 60 files/833 unit + tests, declaration plus CJS/ESM builds with no TypeScript diagnostic, all + Native/Legacy/enhancement/CLI/official-frontend E2E, and the nine-page + VuePress build. Native and Legacy protocol suites each passed 50 consecutive + final-tree runs; frontend, enhancement, and CLI suites each passed 10 + consecutive runs; the watcher-specific regression passed 50; macOS packed + Native and Legacy adapters each passed 20 rounds; and no matching process + remained. The current exact `node-network-devtools-2.0.0.tgz` installs in CJS + and ESM consumers, passes packed Native/Legacy runtime tests plus publish + dry-run, contains 190 files including LICENSE, and has SHA-256 + `dbd24df4d1ff4dda6ac545df0ef95c1287f0cace81d1e26fca08caa5667fce84`. +- 2026-08-28: Final remote audit passed at commit + `231bcae02a9f05763a24c01d064127a71301895a`. GitHub Actions run + [33147572570](https://github.com/GrinZero/node-network-devtools/actions/runs/33147572570) + completed all 18 reusable-workflow jobs plus the top-level `Quality Gate` + successfully. The exact uploaded artifact + `node-network-devtools-tgz-33147572570-1` contains 190 files and has the same + SHA-256 recorded above. Its isolated packed-package controller passed 20 + Native and 20 Legacy rounds on both `windows-latest` and `macos-latest`, with + discovery, target, disposal, and closed-endpoint assertions preserved. Active + repository ruleset + [21711512](https://github.com/GrinZero/node-network-devtools/rules/21711512) + targets `refs/heads/main`, strictly requires `Quality Gate` from GitHub Actions + integration `15368`, and has no bypass actors (`current_user_can_bypass` is + `never`). The release workflow cannot reach `publish` until the same reusable + quality workflow succeeds; it then downloads that workflow's sole artifact and + verifies its SHA-256 and release tag before publishing. No real npm publish was + performed as part of this implementation. +- 2026-08-28: PR #64 manual acceptance audit passed 14/14 cases against exact + packed product commit `449d47db89109e826eb0e7e0584777365eac3f9b` and tarball + SHA-256 + `e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac`. + Computer Use and Playwright exercised six public CLI cases, Native and Legacy + runtime selection, full standard discovery, official Chromium DevTools + request details, reload/reconnect, HTTPS, failed requests, Legacy mocks, SSE, + WebSocket lifecycle/frames, Session/HAR/Replay/Trace, capability boundaries, + and closed-endpoint disposal. The audit discovered that a non-empty HTTP/2 + response crashes Node 24.16 and 26.8 inside + `node:internal/inspector/network_http2`; commit `449d47d` therefore replaced + the open-ended Native capability claim with the verified Node 22.20+ 22.x + range. The exact-package probe proves a complete lifecycle and body on Node + 22.22.3 while the affected/future majors correctly withhold the capability. + The reviewable matrix, 26 privacy-reviewed screenshots, 17 structured + artifacts, five reproduction harnesses, test TLS inputs, and SHA-256 manifest + are retained in the + [PR #64 manual evidence report](test-evidence/v2/pr-64/README.md). diff --git a/docs/v2-migration.md b/docs/v2-migration.md new file mode 100644 index 0000000..c37204a --- /dev/null +++ b/docs/v2-migration.md @@ -0,0 +1,150 @@ +# Migrating to Node Network Devtools v2 + +Version 2 separates runtime selection, network capture, CDP target ownership, and +frontend launching. It keeps the callable v1 cleanup handle, but defaults and +ownership rules are intentionally different. + +## Behavioral changes + +- `register()` no longer opens Chrome by default. Use `devtools.open: true`, + `registration.openDevtools()`, or `nnd dev --open`. +- The project no longer starts, retains, navigates, or kills a Chrome process. +- Legacy application-to-target traffic uses child-process IPC. Port 5270, its + WebSocket bridge, lock file, and health ping no longer exist. +- Native and Legacy are complete, mutually exclusive backends. One request is + never captured by both in a single registration. +- Inspector and Legacy target ports default to `0`, allowing the OS to bind an + available loopback port without a probe/bind race. +- Auto prefers a proven Native runtime, then reports a structured reason when it + uses Legacy. Forced Native never silently falls back. +- The `node-network-devtools/dev` subpath remains as a deprecated compatibility + alias, but now resolves to the same published `dist` entry as the package root. + +## Option mapping + +| v1 option | v2 option | Notes | +| ------------------ | ---------------------- | ---------------------------------------------------- | +| `adapter` | `mode` | `adapter` still works with a deprecation diagnostic. | +| `requiredFeatures` | `requiredCapabilities` | Old name remains compatible. | +| `autoOpenDevtool` | `devtools.open` | Default changed from implicit opening to `false`. | +| `serverPort` | `legacy.serverPort` | Default is now `0`. | +| `intercept` | `legacy.intercept` | Hooks are installed only when Legacy is selected. | +| `port` | Remove | The old application bridge port is unused. | + +Before: + +```ts +const unregister = register({ + serverPort: 5271, + autoOpenDevtool: true, + intercept: { normal: true, fetch: true } +}) +``` + +After: + +```ts +const registration = register({ + mode: 'auto', + devtools: { open: true }, + legacy: { + serverPort: 0, + intercept: { normal: true, fetch: true } + } +}) + +await registration.ready +await registration.dispose() +``` + +The old `unregister()` call remains valid, but `await registration.dispose()` is +preferred when teardown order matters. + +## Backend selection + +Use required capabilities to express behavior the application actually needs: + +```ts +register({ + mode: 'auto', + requiredCapabilities: ['requestBody', 'websocketFrames'] +}) +``` + +Those requirements select Legacy today. Native capability values are derived +from both the running Node version and available Inspector methods. + +Notable verified boundaries: + +- Native network inspection first appears behind the experimental flag in Node + 20.18 and 22.6. +- Auto's proven Native baseline is Node 24.7 and newer. +- Native HTTP/2 is explicitly allowlisted only on Node 22.20+ within the 22.x + line. A non-empty h2c lifecycle passed on Node 22.22.3, but consuming a + non-empty response with `setEncoding()` crashes the upstream experimental + Inspector on Node 24.16.0 and 26.8.1 with `Missing dataLength`. Other and + future majors remain false until independently verified; Legacy HTTP/2 is + also unsupported. +- Node 22.22 can retrieve Native HTTP response bodies, but Fetch + `Network.getResponseBody` returns an empty body in the package E2E. Because the + public `responseBody` capability spans transports, v2 conservatively reports + it as false on Node 22 and true only on the verified Node 24+ baseline. +- Native request bodies, WebSocket frames, SSE message parsing, and Mock are not + advertised. Select Legacy when these are required. + +Node 22 and 24 are LTS and Node 26 is Current at the time this plan was +implemented. Node 18 and 20 are EOL; the package retains `>=18.18` compatibility +and CI lanes so existing applications can migrate, but new deployments should +use a maintained release. + +## Zero-code startup + +Instead of editing the application entry point, prefer: + +```bash +nnd dev --open src/server.js +nnd dev --runner tsx src/server.ts -- --port 3000 +nnd doctor --json +``` + +The CLI supplies the correct preloads and Native flag, supports wait/no-wait and +watch behavior, forwards application arguments/signals, and prints the canonical +target selected by the backend. + +## Session, HAR, Replay, and Mock + +Recording is backend-neutral: + +```ts +const registration = register({ + session: { directory: '.nnd/sessions/run-001', har: true } +}) +await registration.ready +// application traffic +await registration.dispose() +``` + +Disposal closes the recorder and body commands, exports HAR when requested, then +closes the backend even when recording fails. The exact output directory must +not already contain `manifest.json` or `events.ndjson`. + +Replay can validate without I/O or issue real HTTP(S) requests: + +```bash +nnd replay --dry-run --json .nnd/sessions/run-001 +nnd replay capture.har +``` + +Mock rules belong under `legacy.mock`. Configuring them in Auto selects Legacy +with `NND_AUTO_LEGACY_MOCK_REQUIRED`; configuring them with forced Native throws +`NND_NATIVE_MOCK_CONFLICT` before an adapter starts. + +## Release gates + +The v2 gate builds and packs once, then installs that exact tarball into isolated +CJS and ESM consumers. Pull requests run mandatory Ubuntu protocol tests and a +supported runtime matrix; Node 24 also runs the complete Native, Legacy, CLI, +frontend, Session/HAR/Replay/Mock/Trace gates. Windows and macOS run adapter +smoke loops, while the larger OS/runtime matrix runs nightly. Publishing calls +the same reusable quality workflow and publishes the verified tarball with npm +trusted publishing/provenance rather than rebuilding it. diff --git a/output/playwright/pr-64/.gitignore b/output/playwright/pr-64/.gitignore new file mode 100644 index 0000000..9a42538 --- /dev/null +++ b/output/playwright/pr-64/.gitignore @@ -0,0 +1,16 @@ +.runtime/ +.playwright/ +.playwright-cli/ +consumer/ +package/ +videos/ +*.log +screenshots/MT-02-zero-code-native.png +screenshots/MT-04-native-standard-discovery.png +screenshots/MT-07-legacy-fallback-and-capabilities.png +screenshots/MT-08-legacy-standard-discovery.png +screenshots/MT-10-legacy-mock-fetch-no-origin-leak.png +screenshots/MT-10-legacy-mock-http-no-origin-leak.png +screenshots/MT-11-legacy-websocket-frames.png +!manual-localhost-cert.pem +!manual-localhost-key.pem diff --git a/output/playwright/pr-64/artifacts/cli-manual-results.json b/output/playwright/pr-64/artifacts/cli-manual-results.json new file mode 100644 index 0000000..2031f80 --- /dev/null +++ b/output/playwright/pr-64/artifacts/cli-manual-results.json @@ -0,0 +1,291 @@ +{ + "schemaVersion": 1, + "productCommit": "449d47db89109e826eb0e7e0584777365eac3f9b", + "tarballSha256": "e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac", + "cases": [ + { + "id": "doctor", + "title": "CLI version + Native doctor", + "status": "PASS", + "command": "nnd --version && node --experimental-network-inspection /node_modules/.bin/nnd doctor --json", + "actual": { + "version": "2.0.0", + "schemaVersion": 1, + "ok": true, + "nodeVersion": "24.16.0", + "packageVersion": "2.0.0", + "experimentalFlag": true, + "missingRequired": [], + "selection": { + "requested": "auto", + "selected": "native" + }, + "capabilities": { + "http": true, + "https": true, + "fetch": true, + "http2": false, + "responseBody": true, + "requestBody": false, + "websocketLifecycle": true, + "websocketFrames": false, + "sseMessages": false, + "initiator": true + } + } + }, + { + "id": "native", + "title": "Zero-code nnd dev · NATIVE", + "status": "PASS", + "command": "nnd dev --no-wait --mode native /packages/network-debugger/test/e2e/cli/fixtures/probe.mjs", + "actual": { + "type": "fixture-ready", + "preloadInjected": true, + "mode": "native", + "execArgv": [ + "--experimental-network-inspection", + "--inspect=127.0.0.1:0", + "--import=file:///node_modules/node-network-devtools/dist/register.mjs" + ], + "target": { + "id": "1c0c8e41-a808-4721-8aa6-6834bba2c010", + "type": "node", + "discoveryUrl": "http://127.0.0.1:60423/json/list", + "webSocketDebuggerUrl": "ws://127.0.0.1:60423/1c0c8e41-a808-4721-8aa6-6834bba2c010" + }, + "capabilities": { + "http": true, + "https": true, + "fetch": true, + "http2": false, + "responseBody": true, + "requestBody": false, + "websocketLifecycle": true, + "websocketFrames": false, + "sseMessages": false, + "initiator": true + } + } + }, + { + "id": "legacy", + "title": "Zero-code nnd dev · LEGACY", + "status": "PASS", + "command": "nnd dev --no-wait --mode legacy /packages/network-debugger/test/e2e/cli/fixtures/probe.mjs", + "actual": { + "type": "fixture-ready", + "preloadInjected": true, + "mode": "legacy", + "execArgv": [ + "--import=file:///node_modules/node-network-devtools/dist/register.mjs" + ], + "target": { + "id": "node-network-devtools-a426be27-20c0-4b1a-b552-be1414423c06", + "type": "node", + "discoveryUrl": "http://127.0.0.1:60436/json/list", + "webSocketDebuggerUrl": "ws://127.0.0.1:60436/devtools/page/node-network-devtools-a426be27-20c0-4b1a-b552-be1414423c06" + }, + "capabilities": { + "http": true, + "https": true, + "fetch": true, + "http2": false, + "responseBody": true, + "requestBody": true, + "websocketLifecycle": true, + "websocketFrames": true, + "sseMessages": true, + "initiator": true + } + } + }, + { + "id": "conflict", + "title": "Forced Native + Mock conflict", + "status": "PASS", + "command": "node /manual-native-mock-conflict.mjs", + "actual": { + "name": "RuntimeRegistrationError", + "code": "NND_NATIVE_MOCK_CONFLICT", + "message": "Request/response mocking is available only with the Legacy backend.", + "details": { + "mode": "native", + "mockRuleCount": 1, + "hint": "Use mode: \"legacy\" or mode: \"auto\" when legacy.mock rules are configured." + } + } + }, + { + "id": "replay", + "title": "Public nnd replay · dry + real", + "status": "PASS", + "command": "nnd replay --dry-run --json /artifacts/cli-replay-fixture.har && nnd replay --json /artifacts/cli-replay-fixture.har", + "actual": { + "dryRun": { + "dryRun": true, + "startedAt": "2026-08-28T08:00:40.372Z", + "completedAt": "2026-08-28T08:00:40.374Z", + "requests": [ + { + "index": 0, + "requestId": "cli-replay-get", + "method": "GET", + "url": "http://127.0.0.1:60344/replay-get?case=cli-public-replay", + "headers": {} + }, + { + "index": 1, + "requestId": "cli-replay-post", + "method": "POST", + "url": "http://127.0.0.1:60344/replay-post?case=cli-public-replay", + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": "cli-replay-request-body" + } + ], + "results": [ + { + "request": { + "index": 0, + "requestId": "cli-replay-get", + "method": "GET", + "url": "http://127.0.0.1:60344/replay-get?case=cli-public-replay", + "headers": {} + }, + "dryRun": true, + "ok": true, + "durationMs": 0 + }, + { + "request": { + "index": 1, + "requestId": "cli-replay-post", + "method": "POST", + "url": "http://127.0.0.1:60344/replay-post?case=cli-public-replay", + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": "cli-replay-request-body" + }, + "dryRun": true, + "ok": true, + "durationMs": 0 + } + ], + "succeeded": 2, + "failed": 0 + }, + "real": { + "dryRun": false, + "startedAt": "2026-08-28T08:00:40.450Z", + "completedAt": "2026-08-28T08:00:40.463Z", + "requests": [ + { + "index": 0, + "requestId": "cli-replay-get", + "method": "GET", + "url": "http://127.0.0.1:60344/replay-get?case=cli-public-replay", + "headers": {} + }, + { + "index": 1, + "requestId": "cli-replay-post", + "method": "POST", + "url": "http://127.0.0.1:60344/replay-post?case=cli-public-replay", + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": "cli-replay-request-body" + } + ], + "results": [ + { + "request": { + "index": 0, + "requestId": "cli-replay-get", + "method": "GET", + "url": "http://127.0.0.1:60344/replay-get?case=cli-public-replay", + "headers": {} + }, + "dryRun": false, + "ok": true, + "status": 200, + "statusText": "OK", + "responseHeaders": { + "connection": "keep-alive", + "content-type": "text/plain; charset=utf-8", + "date": "Fri, 28 Aug 2026 08:00:40 GMT", + "keep-alive": "timeout=5", + "transfer-encoding": "chunked" + }, + "durationMs": 8.241875000000007 + }, + { + "request": { + "index": 1, + "requestId": "cli-replay-post", + "method": "POST", + "url": "http://127.0.0.1:60344/replay-post?case=cli-public-replay", + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": "cli-replay-request-body" + }, + "dryRun": false, + "ok": true, + "status": 201, + "statusText": "Created", + "responseHeaders": { + "connection": "keep-alive", + "content-type": "application/json; charset=utf-8", + "date": "Fri, 28 Aug 2026 08:00:40 GMT", + "keep-alive": "timeout=5", + "transfer-encoding": "chunked" + }, + "durationMs": 3.082583999999997 + } + ], + "succeeded": 2, + "failed": 0 + }, + "originRequests": [ + { + "method": "GET", + "url": "/replay-get?case=cli-public-replay", + "body": "" + }, + { + "method": "POST", + "url": "/replay-post?case=cli-public-replay", + "body": "cli-replay-request-body" + } + ] + } + }, + { + "id": "open", + "title": "nnd dev --open · authoritative target", + "status": "PASS", + "command": "nnd dev --open --mode native /packages/network-debugger/test/e2e/cli/fixtures/probe.mjs", + "actual": { + "preloadInjected": true, + "mode": "native", + "inspectWait": "--inspect-wait=127.0.0.1:0", + "target": { + "id": "7e52e8aa-c81d-45c8-ad04-26d18b69cbca", + "type": "node", + "discoveryUrl": "http://127.0.0.1:60572/json/list", + "webSocketDebuggerUrl": "ws://127.0.0.1:60572/7e52e8aa-c81d-45c8-ad04-26d18b69cbca" + }, + "openedExactlyOnce": true, + "openedFrontend": { + "frontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:60572/7e52e8aa-c81d-45c8-ad04-26d18b69cbca", + "webSocketDebuggerUrl": "ws://127.0.0.1:60572/7e52e8aa-c81d-45c8-ad04-26d18b69cbca" + }, + "launcherVerification": "OS browser spawn redirected to an exact-target CDP verifier" + } + } + ] +} diff --git a/output/playwright/pr-64/artifacts/cli-replay-fixture.har b/output/playwright/pr-64/artifacts/cli-replay-fixture.har new file mode 100644 index 0000000..9b9b088 --- /dev/null +++ b/output/playwright/pr-64/artifacts/cli-replay-fixture.har @@ -0,0 +1,63 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "PR #64 manual evidence", + "version": "1" + }, + "pages": [], + "entries": [ + { + "request": { + "method": "GET", + "url": "http://127.0.0.1:60344/replay-get?case=cli-public-replay", + "httpVersion": "HTTP/1.1", + "headers": [], + "queryString": [ + { + "name": "case", + "value": "cli-public-replay" + } + ], + "cookies": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": {}, + "cache": {}, + "timings": {}, + "_requestId": "cli-replay-get" + }, + { + "request": { + "method": "POST", + "url": "http://127.0.0.1:60344/replay-post?case=cli-public-replay", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + } + ], + "queryString": [ + { + "name": "case", + "value": "cli-public-replay" + } + ], + "cookies": [], + "headersSize": -1, + "bodySize": 22, + "postData": { + "mimeType": "text/plain; charset=utf-8", + "text": "cli-replay-request-body" + } + }, + "response": {}, + "cache": {}, + "timings": {}, + "_requestId": "cli-replay-post" + } + ] + } +} diff --git a/output/playwright/pr-64/artifacts/legacy-dispose-summary.json b/output/playwright/pr-64/artifacts/legacy-dispose-summary.json new file mode 100644 index 0000000..7f17c58 --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-dispose-summary.json @@ -0,0 +1,5 @@ +{ + "registrationState": "disposed", + "discoveryClosed": true, + "targetSocketClosed": true +} diff --git a/output/playwright/pr-64/artifacts/legacy-events.ndjson b/output/playwright/pr-64/artifacts/legacy-events.ndjson new file mode 100644 index 0000000..ffec96f --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-events.ndjson @@ -0,0 +1,54 @@ +{"schemaVersion":1,"sequence":1,"recordedAt":"2026-08-28T07:50:35.526Z","method":"Debugger.scriptParsed","params":{"url":"file:///manual-evidence-server.mjs","scriptLanguage":"JavaScript","embedderName":"file:///manual-evidence-server.mjs","scriptId":"1","sourceMapURL":"","hasSourceURL":false}} +{"schemaVersion":1,"sequence":2,"recordedAt":"2026-08-28T07:50:35.528Z","method":"Network.requestWillBeSent","params":{"requestId":"0cc5f65a-40ac-4130-9be9-3f41d81ff929","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59430/json/list","request":{"url":"http://127.0.0.1:59430/json/list","method":"GET","headers":{},"initialPriority":"High","mixedContentType":"none"},"timestamp":0.009,"wallTime":1787903435.508,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":3,"functionName":"","lineNumber":391,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":5,"functionName":"process.processTicksAndRejections","lineNumber":104,"url":"node:internal/process/task_queues"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":3,"recordedAt":"2026-08-28T07:50:35.529Z","method":"Network.requestWillBeSent","params":{"requestId":"9405f647-edce-4fb1-a015-9e7b89878de5","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59430/json/version","request":{"url":"http://127.0.0.1:59430/json/version","method":"GET","headers":{},"initialPriority":"High","mixedContentType":"none"},"timestamp":0.011,"wallTime":1787903435.512,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":3,"functionName":"","lineNumber":392,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":5,"functionName":"process.processTicksAndRejections","lineNumber":104,"url":"node:internal/process/task_queues"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":4,"recordedAt":"2026-08-28T07:50:35.530Z","method":"Network.requestWillBeSent","params":{"requestId":"9e61ccb6-be11-43f9-8355-2d64a4cb6e60","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59430/json/protocol","request":{"url":"http://127.0.0.1:59430/json/protocol","method":"GET","headers":{},"initialPriority":"High","mixedContentType":"none"},"timestamp":0.011,"wallTime":1787903435.512,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":3,"functionName":"","lineNumber":393,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":5,"functionName":"process.processTicksAndRejections","lineNumber":104,"url":"node:internal/process/task_queues"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":5,"recordedAt":"2026-08-28T07:50:35.530Z","method":"Network.responseReceived","params":{"requestId":"0cc5f65a-40ac-4130-9be9-3f41d81ff929","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":0.017,"type":"Other","response":{"url":"http://127.0.0.1:59430/json/list","status":200,"statusText":"OK","headers":{"access-control-allow-origin":"*","cache-control":"no-store","connection":"keep-alive","content-type":"application/json; charset=utf-8","date":"Fri, 28 Aug 2026 07:50:35 GMT","keep-alive":"timeout=5","transfer-encoding":"chunked"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":6,"recordedAt":"2026-08-28T07:50:35.530Z","method":"Network.dataReceived","params":{"requestId":"0cc5f65a-40ac-4130-9be9-3f41d81ff929","timestamp":0.018,"dataLength":821,"encodedDataLength":821}} +{"schemaVersion":1,"sequence":7,"recordedAt":"2026-08-28T07:50:35.531Z","method":"Network.loadingFinished","params":{"requestId":"0cc5f65a-40ac-4130-9be9-3f41d81ff929","timestamp":0.018,"encodedDataLength":821}} +{"schemaVersion":1,"sequence":8,"recordedAt":"2026-08-28T07:50:35.536Z","method":"Network.responseReceived","params":{"requestId":"9405f647-edce-4fb1-a015-9e7b89878de5","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":0.019,"type":"Other","response":{"url":"http://127.0.0.1:59430/json/version","status":200,"statusText":"OK","headers":{"access-control-allow-origin":"*","cache-control":"no-store","connection":"keep-alive","content-type":"application/json; charset=utf-8","date":"Fri, 28 Aug 2026 07:50:35 GMT","keep-alive":"timeout=5","transfer-encoding":"chunked"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":9,"recordedAt":"2026-08-28T07:50:35.537Z","method":"Network.dataReceived","params":{"requestId":"9405f647-edce-4fb1-a015-9e7b89878de5","timestamp":0.019,"dataLength":248,"encodedDataLength":248}} +{"schemaVersion":1,"sequence":10,"recordedAt":"2026-08-28T07:50:35.537Z","method":"Network.loadingFinished","params":{"requestId":"9405f647-edce-4fb1-a015-9e7b89878de5","timestamp":0.019,"encodedDataLength":248}} +{"schemaVersion":1,"sequence":11,"recordedAt":"2026-08-28T07:50:35.538Z","method":"Network.responseReceived","params":{"requestId":"9e61ccb6-be11-43f9-8355-2d64a4cb6e60","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":0.019,"type":"Other","response":{"url":"http://127.0.0.1:59430/json/protocol","status":200,"statusText":"OK","headers":{"access-control-allow-origin":"*","cache-control":"no-store","connection":"keep-alive","content-type":"application/json; charset=utf-8","date":"Fri, 28 Aug 2026 07:50:35 GMT","keep-alive":"timeout=5","transfer-encoding":"chunked"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":12,"recordedAt":"2026-08-28T07:50:35.539Z","method":"Network.dataReceived","params":{"requestId":"9e61ccb6-be11-43f9-8355-2d64a4cb6e60","timestamp":0.019,"dataLength":1323,"encodedDataLength":1323}} +{"schemaVersion":1,"sequence":13,"recordedAt":"2026-08-28T07:50:35.539Z","method":"Network.loadingFinished","params":{"requestId":"9e61ccb6-be11-43f9-8355-2d64a4cb6e60","timestamp":0.019,"encodedDataLength":1323}} +{"schemaVersion":1,"sequence":14,"recordedAt":"2026-08-28T07:51:15.497Z","method":"Debugger.scriptParsed","params":{"url":"file:///manual-evidence-server.mjs","scriptLanguage":"JavaScript","embedderName":"file:///manual-evidence-server.mjs","scriptId":"1","sourceMapURL":"","hasSourceURL":false}} +{"schemaVersion":1,"sequence":15,"recordedAt":"2026-08-28T07:51:54.793Z","method":"Network.requestWillBeSent","params":{"requestId":"58134218-34a0-4b54-a2ab-e1fd65450177","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/get?token=legacy-http-get-01","request":{"url":"http://127.0.0.1:59428/get?token=legacy-http-get-01","method":"GET","headers":{"host":"127.0.0.1:59428"},"initialPriority":"High","mixedContentType":"none"},"timestamp":79.291,"wallTime":1787903514.791,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":45,"functionName":"","lineNumber":114,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"httpRequest","lineNumber":112,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":22,"functionName":"runScenario","lineNumber":494,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":16,"recordedAt":"2026-08-28T07:51:54.796Z","method":"Network.responseReceived","params":{"requestId":"58134218-34a0-4b54-a2ab-e1fd65450177","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":79.292,"type":"Other","response":{"url":"http://127.0.0.1:59428/get?token=legacy-http-get-01","status":200,"statusText":"OK","headers":{"content-type":"text/plain; charset=utf-8","content-length":"38","x-manual-case":"legacy-http-get-01","date":"Fri, 28 Aug 2026 07:51:54 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"text/plain"}}} +{"schemaVersion":1,"sequence":17,"recordedAt":"2026-08-28T07:51:54.797Z","method":"Network.dataReceived","params":{"requestId":"58134218-34a0-4b54-a2ab-e1fd65450177","timestamp":79.293,"dataLength":38,"encodedDataLength":38}} +{"schemaVersion":1,"sequence":18,"recordedAt":"2026-08-28T07:51:54.798Z","method":"Network.loadingFinished","params":{"requestId":"58134218-34a0-4b54-a2ab-e1fd65450177","timestamp":79.293,"encodedDataLength":38}} +{"schemaVersion":1,"sequence":19,"recordedAt":"2026-08-28T07:51:58.752Z","method":"Network.requestWillBeSent","params":{"requestId":"679ed0e5-ef59-4d2e-89ab-391343c82c31","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"https://127.0.0.1:59429/secure-get?token=legacy-https-get-02","request":{"url":"https://127.0.0.1:59429/secure-get?token=legacy-https-get-02","method":"GET","headers":{"host":"127.0.0.1:59429"},"initialPriority":"High","mixedContentType":"none"},"timestamp":83.251,"wallTime":1787903518.749,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":45,"functionName":"","lineNumber":114,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"httpRequest","lineNumber":112,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":22,"functionName":"runScenario","lineNumber":497,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":20,"recordedAt":"2026-08-28T07:51:58.760Z","method":"Network.responseReceived","params":{"requestId":"679ed0e5-ef59-4d2e-89ab-391343c82c31","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":83.257,"type":"Other","response":{"url":"https://127.0.0.1:59429/secure-get?token=legacy-https-get-02","status":200,"statusText":"OK","headers":{"content-type":"text/plain; charset=utf-8","content-length":"41","x-manual-case":"legacy-https-get-02","date":"Fri, 28 Aug 2026 07:51:58 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"text/plain"}}} +{"schemaVersion":1,"sequence":21,"recordedAt":"2026-08-28T07:51:58.762Z","method":"Network.dataReceived","params":{"requestId":"679ed0e5-ef59-4d2e-89ab-391343c82c31","timestamp":83.257,"dataLength":41,"encodedDataLength":41}} +{"schemaVersion":1,"sequence":22,"recordedAt":"2026-08-28T07:51:58.763Z","method":"Network.loadingFinished","params":{"requestId":"679ed0e5-ef59-4d2e-89ab-391343c82c31","timestamp":83.257,"encodedDataLength":41}} +{"schemaVersion":1,"sequence":23,"recordedAt":"2026-08-28T07:52:02.894Z","method":"Network.requestWillBeSent","params":{"requestId":"3e47e0b7-987b-463c-aa0e-9765fc6d7e11","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/post?token=legacy-fetch-post-03","request":{"url":"http://127.0.0.1:59428/post?token=legacy-fetch-post-03","method":"POST","headers":{"content-type":"text/plain; charset=utf-8","x-manual-case":"legacy-fetch-post-03"},"initialPriority":"High","mixedContentType":"none","postData":"manual-request-body:legacy-fetch-post-03","hasPostData":true},"timestamp":87.391,"wallTime":1787903522.892,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":30,"functionName":"runScenario","lineNumber":502,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":24,"recordedAt":"2026-08-28T07:52:02.900Z","method":"Network.responseReceived","params":{"requestId":"3e47e0b7-987b-463c-aa0e-9765fc6d7e11","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":87.396,"type":"Other","response":{"url":"http://127.0.0.1:59428/post?token=legacy-fetch-post-03","status":201,"statusText":"Created","headers":{"connection":"keep-alive","content-length":"89","content-type":"application/json; charset=utf-8","date":"Fri, 28 Aug 2026 07:52:02 GMT","keep-alive":"timeout=5","x-manual-case":"legacy-fetch-post-03"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":25,"recordedAt":"2026-08-28T07:52:02.901Z","method":"Network.dataReceived","params":{"requestId":"3e47e0b7-987b-463c-aa0e-9765fc6d7e11","timestamp":87.396,"dataLength":89,"encodedDataLength":89}} +{"schemaVersion":1,"sequence":26,"recordedAt":"2026-08-28T07:52:02.902Z","method":"Network.loadingFinished","params":{"requestId":"3e47e0b7-987b-463c-aa0e-9765fc6d7e11","timestamp":87.397,"encodedDataLength":89}} +{"schemaVersion":1,"sequence":27,"recordedAt":"2026-08-28T07:52:06.968Z","method":"Network.requestWillBeSent","params":{"requestId":"1b1612b8-6e03-4483-8d4f-74c232a36835","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/trace?token=legacy-trace-04","request":{"url":"http://127.0.0.1:59428/trace?token=legacy-trace-04","method":"GET","headers":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01","tracestate":"vendor=manual-pr64","host":"127.0.0.1:59428"},"initialPriority":"High","mixedContentType":"none"},"timestamp":91.466,"wallTime":1787903526.966,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":45,"functionName":"","lineNumber":114,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"httpRequest","lineNumber":112,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":22,"functionName":"runScenario","lineNumber":538,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":28,"recordedAt":"2026-08-28T07:52:06.971Z","method":"Network.responseReceived","params":{"requestId":"1b1612b8-6e03-4483-8d4f-74c232a36835","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":91.468,"type":"Other","response":{"url":"http://127.0.0.1:59428/trace?token=legacy-trace-04","status":200,"statusText":"OK","headers":{"content-type":"application/json; charset=utf-8","content-length":"133","date":"Fri, 28 Aug 2026 07:52:06 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":29,"recordedAt":"2026-08-28T07:52:06.971Z","method":"Network.dataReceived","params":{"requestId":"1b1612b8-6e03-4483-8d4f-74c232a36835","timestamp":91.468,"dataLength":133,"encodedDataLength":133}} +{"schemaVersion":1,"sequence":30,"recordedAt":"2026-08-28T07:52:06.972Z","method":"Network.loadingFinished","params":{"requestId":"1b1612b8-6e03-4483-8d4f-74c232a36835","timestamp":91.468,"encodedDataLength":133}} +{"schemaVersion":1,"sequence":31,"recordedAt":"2026-08-28T07:52:10.934Z","method":"Network.requestWillBeSent","params":{"requestId":"25379421-e6bd-43f0-92a2-eb9f2345a4ac","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/reset?token=legacy-failed-05","request":{"url":"http://127.0.0.1:59428/reset?token=legacy-failed-05","method":"GET","headers":{"host":"127.0.0.1:59428"},"initialPriority":"High","mixedContentType":"none"},"timestamp":95.432,"wallTime":1787903530.932,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":45,"functionName":"","lineNumber":114,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"httpRequest","lineNumber":112,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":15,"functionName":"runScenario","lineNumber":546,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":32,"recordedAt":"2026-08-28T07:52:10.936Z","method":"Network.loadingFailed","params":{"requestId":"25379421-e6bd-43f0-92a2-eb9f2345a4ac","timestamp":95.433,"type":"Fetch","errorText":"socket hang up","canceled":false}} +{"schemaVersion":1,"sequence":33,"recordedAt":"2026-08-28T07:52:14.934Z","method":"Network.requestWillBeSent","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/sse?token=legacy-sse-06","request":{"url":"http://127.0.0.1:59428/sse?token=legacy-sse-06","method":"GET","headers":{},"initialPriority":"High","mixedContentType":"none"},"timestamp":99.432,"wallTime":1787903534.932,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":30,"functionName":"runScenario","lineNumber":559,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":34,"recordedAt":"2026-08-28T07:52:14.938Z","method":"Network.responseReceived","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":99.435,"type":"EventSource","response":{"url":"http://127.0.0.1:59428/sse?token=legacy-sse-06","status":200,"statusText":"OK","headers":{"cache-control":"no-cache","connection":"keep-alive","content-length":"81","content-type":"text/event-stream; charset=utf-8","date":"Fri, 28 Aug 2026 07:52:14 GMT","keep-alive":"timeout=5"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"text/event-stream"}}} +{"schemaVersion":1,"sequence":35,"recordedAt":"2026-08-28T07:52:14.939Z","method":"Network.eventSourceMessageReceived","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","timestamp":99.435,"eventName":"manual","eventId":"1","data":"sse-legacy-sse-06"}} +{"schemaVersion":1,"sequence":36,"recordedAt":"2026-08-28T07:52:14.940Z","method":"Network.eventSourceMessageReceived","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","timestamp":99.435,"eventName":"message","eventId":"2","data":"complete-legacy-sse-06"}} +{"schemaVersion":1,"sequence":37,"recordedAt":"2026-08-28T07:52:14.940Z","method":"Network.dataReceived","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","timestamp":99.435,"dataLength":81,"encodedDataLength":81}} +{"schemaVersion":1,"sequence":38,"recordedAt":"2026-08-28T07:52:14.941Z","method":"Network.loadingFinished","params":{"requestId":"21870c2f-ae5c-45b6-a61b-f2827cf4b8fe","timestamp":99.435,"encodedDataLength":81}} +{"schemaVersion":1,"sequence":39,"recordedAt":"2026-08-28T07:52:18.995Z","method":"Network.webSocketCreated","params":{"url":"ws://127.0.0.1:59428/websocket?token=legacy-websocket-07","initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":20,"functionName":"","lineNumber":138,"url":"file:///manual-evidence-server.mjs"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"webSocketRoundTrip","lineNumber":137,"url":"file:///manual-evidence-server.mjs"},{"columnNumber":92,"functionName":"runScenario","lineNumber":565,"url":"file:///manual-evidence-server.mjs"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8"}} +{"schemaVersion":1,"sequence":40,"recordedAt":"2026-08-28T07:52:18.998Z","method":"Network.webSocketWillSendHandshakeRequest","params":{"wallTime":1787903538.994,"timestamp":103.493,"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","request":{"headers":{"sec-websocket-version":13,"sec-websocket-key":"zt2WioYgquUhnot+SKWt1w==","connection":"Upgrade","upgrade":"websocket","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","host":"127.0.0.1:59428"}}}} +{"schemaVersion":1,"sequence":41,"recordedAt":"2026-08-28T07:52:18.999Z","method":"Network.webSocketHandshakeResponseReceived","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","response":{"headers":{"Upgrade":"websocket","Connection":"Upgrade","Sec-WebSocket-Accept":"V0zn5isdT7rrXXw2VyDyJ+iftJE="},"headersText":"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: V0zn5isdT7rrXXw2VyDyJ+iftJE=","status":101,"statusText":"Switching Protocols","requestHeadersText":"GET ws://127.0.0.1:59428/websocket?token=legacy-websocket-07 HTTP/1.1\r\nsec-websocket-version: 13\r\nsec-websocket-key: zt2WioYgquUhnot+SKWt1w==\r\nconnection: Upgrade\r\nupgrade: websocket\r\nsec-websocket-extensions: permessage-deflate; client_max_window_bits\r\nhost: 127.0.0.1:59428","requestHeaders":{"sec-websocket-version":"13","sec-websocket-key":"zt2WioYgquUhnot+SKWt1w==","connection":"Upgrade","upgrade":"websocket","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","host":"127.0.0.1:59428"}},"timestamp":103.493}} +{"schemaVersion":1,"sequence":42,"recordedAt":"2026-08-28T07:52:19.000Z","method":"Network.webSocketFrameSent","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","response":{"payloadData":"client-text:legacy-websocket-07","opcode":1,"mask":true},"timestamp":103.494}} +{"schemaVersion":1,"sequence":43,"recordedAt":"2026-08-28T07:52:19.001Z","method":"Network.webSocketFrameSent","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","response":{"payloadData":"AAECf4D+/w==","opcode":2,"mask":true},"timestamp":103.494}} +{"schemaVersion":1,"sequence":44,"recordedAt":"2026-08-28T07:52:19.002Z","method":"Network.webSocketFrameReceived","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","response":{"payloadData":"client-text:legacy-websocket-07","opcode":1,"mask":false},"timestamp":103.494}} +{"schemaVersion":1,"sequence":45,"recordedAt":"2026-08-28T07:52:19.003Z","method":"Network.webSocketFrameReceived","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","response":{"payloadData":"AAECf4D+/w==","opcode":2,"mask":false},"timestamp":103.495}} +{"schemaVersion":1,"sequence":46,"recordedAt":"2026-08-28T07:52:19.003Z","method":"Network.webSocketClosed","params":{"requestId":"ef2b1aa4-f871-4c37-a398-91a29ed42bd8","timestamp":103.495}} +{"schemaVersion":1,"sequence":47,"recordedAt":"2026-08-28T07:52:22.993Z","method":"Network.requestWillBeSent","params":{"requestId":"97ab50a1-7dc0-4698-8eb6-6c585c1b0800","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08","request":{"url":"http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08","method":"GET","headers":{"host":"127.0.0.1:59428"},"initialPriority":"High","mixedContentType":"none"},"timestamp":107.491,"wallTime":1787903542.991,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":45,"functionName":"","lineNumber":114,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":0,"functionName":"new Promise","lineNumber":0,"url":""},{"columnNumber":10,"functionName":"httpRequest","lineNumber":112,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":22,"functionName":"runScenario","lineNumber":516,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":48,"recordedAt":"2026-08-28T07:52:22.994Z","method":"Network.responseReceived","params":{"requestId":"97ab50a1-7dc0-4698-8eb6-6c585c1b0800","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":107.491,"type":"Other","response":{"url":"http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08","status":207,"statusText":"Manual Mock HTTP","headers":{"content-type":"application/json; charset=utf-8","x-nnd-mock":"http","content-length":"67"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":49,"recordedAt":"2026-08-28T07:52:22.995Z","method":"Network.dataReceived","params":{"requestId":"97ab50a1-7dc0-4698-8eb6-6c585c1b0800","timestamp":107.491,"dataLength":67,"encodedDataLength":67}} +{"schemaVersion":1,"sequence":50,"recordedAt":"2026-08-28T07:52:22.996Z","method":"Network.loadingFinished","params":{"requestId":"97ab50a1-7dc0-4698-8eb6-6c585c1b0800","timestamp":107.491,"encodedDataLength":67}} +{"schemaVersion":1,"sequence":51,"recordedAt":"2026-08-28T07:52:26.992Z","method":"Network.requestWillBeSent","params":{"requestId":"a85e4f7f-a238-44c7-866e-2b7cfb325da1","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","documentURL":"http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09","request":{"url":"http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09","method":"POST","headers":{"content-type":"text/plain","x-manual-mock":"fetch"},"initialPriority":"High","mixedContentType":"none","postData":"must-not-reach-origin:legacy-mock-fetch-09","hasPostData":true},"timestamp":111.49,"wallTime":1787903546.991,"initiator":{"type":"script","stack":{"callFrames":[{"columnNumber":30,"functionName":"runScenario","lineNumber":520,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":41,"functionName":"","lineNumber":983,"url":"file:///manual-evidence-server.mjs","scriptId":"1"},{"columnNumber":28,"functionName":"Server.emit","lineNumber":509,"url":"node:events"},{"columnNumber":12,"functionName":"parserOnIncoming","lineNumber":1226,"url":"node:_http_server"},{"columnNumber":17,"functionName":"HTTPParser.parserOnHeadersComplete","lineNumber":125,"url":"node:_http_common"}]}},"type":"Fetch"}} +{"schemaVersion":1,"sequence":52,"recordedAt":"2026-08-28T07:52:26.994Z","method":"Network.responseReceived","params":{"requestId":"a85e4f7f-a238-44c7-866e-2b7cfb325da1","frameId":"nnd.legacy.frame","loaderId":"nnd.legacy.loader","timestamp":111.491,"type":"Other","response":{"url":"http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09","status":202,"statusText":"Manual Mock Fetch","headers":{"content-type":"application/json; charset=utf-8","x-nnd-mock":"fetch"},"connectionReused":false,"encodedDataLength":0,"charset":"utf-8","mimeType":"application/json"}}} +{"schemaVersion":1,"sequence":53,"recordedAt":"2026-08-28T07:52:26.994Z","method":"Network.dataReceived","params":{"requestId":"a85e4f7f-a238-44c7-866e-2b7cfb325da1","timestamp":111.491,"dataLength":68,"encodedDataLength":68}} +{"schemaVersion":1,"sequence":54,"recordedAt":"2026-08-28T07:52:26.995Z","method":"Network.loadingFinished","params":{"requestId":"a85e4f7f-a238-44c7-866e-2b7cfb325da1","timestamp":111.491,"encodedDataLength":68}} diff --git a/output/playwright/pr-64/artifacts/legacy-finalize-summary.json b/output/playwright/pr-64/artifacts/legacy-finalize-summary.json new file mode 100644 index 0000000..cf339f5 --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-finalize-summary.json @@ -0,0 +1,90 @@ +{ + "sessionDirectory": "/.runtime/legacy-session-1787903435249", + "manualAssertions": { + "passed": true, + "discovery": { + "list": { + "ok": true, + "targetCount": 1, + "targetIdMatches": true + }, + "version": { + "ok": true, + "browser": "node-network-devtools/2", + "protocolVersion": "1.3" + }, + "protocol": { + "ok": true, + "domainCount": 4, + "networkDomain": true, + "networkCommandCount": 7, + "networkEventCount": 12 + } + }, + "failedLifecycle": { + "requestId": "25379421-e6bd-43f0-92a2-eb9f2345a4ac", + "requestWillBeSent": 1, + "responseReceived": 0, + "loadingFinished": 0, + "loadingFailed": 1 + }, + "webSocketBoundary": { + "lifecycleCreated": 1, + "lifecycleClosed": 1, + "framesSent": 2, + "framesReceived": 2, + "expectedFrameCapture": true + }, + "sseBoundary": { + "requestCaptured": true, + "messageEvents": 2, + "expectedMessageCapture": true + }, + "traceBoundary": { + "explicitTraceRequests": 1, + "explicitTracePreserved": true, + "untracedBusinessRequests": 5, + "untracedHeadersAbsent": true + } + }, + "manifest": { + "schemaVersion": 1, + "state": "completed", + "stats": { + "eventCount": 54, + "requestCount": 12, + "bodyCount": 10, + "bodyErrorCount": 0, + "failedRequestCount": 1 + }, + "issues": [], + "traceContexts": [ + { + "requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64" + } + ] + }, + "har": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "entries": 12, + "statuses": [200, 200, 200, 200, 200, 201, 200, 0, 200, 0, 207, 202], + "replayableEntries": 6 + }, + "replay": { + "dryRun": true, + "dryRunRequests": 6, + "dryRunPassed": true, + "realRequests": 6, + "realPassed": true + }, + "originLeakCount": 0 +} diff --git a/output/playwright/pr-64/artifacts/legacy-replayable.har b/output/playwright/pr-64/artifacts/legacy-replayable.har new file mode 100644 index 0000000..69afa0c --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-replayable.har @@ -0,0 +1,458 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2026-08-28T07:51:54.791Z", + "time": 2.0000000000095497, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/get?token=legacy-http-get-01", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-http-get-01" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "legacy-http-get-01" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:51:54 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:legacy-http-get-01" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.0000000000047748, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "58134218-34a0-4b54-a2ab-e1fd65450177" + }, + { + "startedDateTime": "2026-08-28T07:52:02.892Z", + "time": 6.000000000000227, + "request": { + "method": "POST", + "url": "http://127.0.0.1:59428/post?token=legacy-fetch-post-03", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "x-manual-case", + "value": "legacy-fetch-post-03" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-fetch-post-03" + } + ], + "postData": { + "mimeType": "text/plain; charset=utf-8", + "text": "manual-request-body:legacy-fetch-post-03" + }, + "headersSize": -1, + "bodySize": 40 + }, + "response": { + "status": 201, + "statusText": "Created", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "89" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:02 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + }, + { + "name": "x-manual-case", + "value": "legacy-fetch-post-03" + } + ], + "content": { + "size": 89, + "mimeType": "application/json", + "text": "{\"token\":\"legacy-fetch-post-03\",\"requestBody\":\"manual-request-body:legacy-fetch-post-03\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 89 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 4.9999999999954525, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "3e47e0b7-987b-463c-aa0e-9765fc6d7e11" + }, + { + "startedDateTime": "2026-08-28T07:52:06.966Z", + "time": 2.0000000000095497, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/trace?token=legacy-trace-04", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "traceparent", + "value": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + }, + { + "name": "tracestate", + "value": "vendor=manual-pr64" + }, + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-trace-04" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "133" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:06 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 133, + "mimeType": "application/json", + "text": "{\"token\":\"legacy-trace-04\",\"traceparent\":\"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\",\"tracestate\":\"vendor=manual-pr64\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 133 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.0000000000095497, + "receive": 0, + "ssl": -1 + }, + "_requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "_trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + } + }, + { + "startedDateTime": "2026-08-28T07:52:14.932Z", + "time": 3.0000000000001137, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/sse?token=legacy-sse-06", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [ + { + "name": "token", + "value": "legacy-sse-06" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "81" + }, + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:14 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 81, + "mimeType": "text/event-stream", + "text": "id: 1\nevent: manual\ndata: sse-legacy-sse-06\n\nid: 2\ndata: complete-legacy-sse-06\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 81 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 3.0000000000001137, + "receive": 0, + "ssl": -1 + }, + "_requestId": "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe" + }, + { + "startedDateTime": "2026-08-28T07:52:22.991Z", + "time": 0, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-mock-http-08" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 207, + "statusText": "Manual Mock HTTP", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "x-nnd-mock", + "value": "http" + }, + { + "name": "content-length", + "value": "67" + } + ], + "content": { + "size": 67, + "mimeType": "application/json", + "text": "{\"mocked\":true,\"transport\":\"http\",\"source\":\"node-network-devtools\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 67 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "97ab50a1-7dc0-4698-8eb6-6c585c1b0800" + }, + { + "startedDateTime": "2026-08-28T07:52:26.991Z", + "time": 1.0000000000047748, + "request": { + "method": "POST", + "url": "http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain" + }, + { + "name": "x-manual-mock", + "value": "fetch" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-mock-fetch-09" + } + ], + "postData": { + "mimeType": "text/plain", + "text": "must-not-reach-origin:legacy-mock-fetch-09" + }, + "headersSize": -1, + "bodySize": 42 + }, + "response": { + "status": 202, + "statusText": "Manual Mock Fetch", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "x-nnd-mock", + "value": "fetch" + } + ], + "content": { + "size": 68, + "mimeType": "application/json", + "text": "{\"mocked\":true,\"transport\":\"fetch\",\"source\":\"node-network-devtools\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 68 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.0000000000047748, + "receive": 0, + "ssl": -1 + }, + "_requestId": "a85e4f7f-a238-44c7-866e-2b7cfb325da1" + } + ] + } +} diff --git a/output/playwright/pr-64/artifacts/legacy-runtime.json b/output/playwright/pr-64/artifacts/legacy-runtime.json new file mode 100644 index 0000000..d865da6 --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-runtime.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "productCommit": "449d47db89109e826eb0e7e0584777365eac3f9b", + "package": { + "name": "node-network-devtools", + "version": "2.0.0" + }, + "tarballSha256": "e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac", + "node": "v24.16.0", + "platform": "darwin-arm64", + "backend": "legacy", + "selectedMode": "legacy", + "fallbackReason": { + "code": "NND_AUTO_LEGACY_MOCK_REQUIRED", + "level": "info", + "message": "Auto selected Legacy because request/response mocking was configured.", + "hint": "Remove legacy.mock to allow Auto to select the Native backend." + }, + "capabilities": { + "http": true, + "https": true, + "fetch": true, + "http2": false, + "responseBody": true, + "requestBody": true, + "websocketLifecycle": true, + "websocketFrames": true, + "sseMessages": true, + "initiator": true + }, + "target": { + "id": "node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "title": "Node Network Devtools (Legacy)", + "type": "node", + "url": "file:///node_modules/node-network-devtools/dist/fork", + "webSocketDebuggerUrl": "ws://127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "discoveryUrl": "http://127.0.0.1:59430/json/list" + }, + "discovery": [ + { + "id": "node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "title": "Node Network Devtools (Legacy)", + "type": "node", + "url": "file:///node_modules/node-network-devtools/dist/fork", + "webSocketDebuggerUrl": "ws://127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "discoveryUrl": "http://127.0.0.1:59430/json/list" + } + ], + "discoveryContract": { + "list": { + "ok": true, + "targetCount": 1, + "targetIdMatches": true + }, + "version": { + "ok": true, + "browser": "node-network-devtools/2", + "protocolVersion": "1.3" + }, + "protocol": { + "ok": true, + "domainCount": 4, + "networkDomain": true, + "networkCommandCount": 7, + "networkEventCount": 12 + } + }, + "frontendUrl": "http://127.0.0.1:59435/devtools/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68&hl=en-US", + "controlUrl": "http://127.0.0.1:59436", + "originalFunctionsPreserved": { + "fetch": false, + "httpRequest": false, + "httpsRequest": false + }, + "sourceSha256": "46f81eb2930040a31f402cf4cb113758628eb72c9db27617babba1d3df31af27" +} diff --git a/output/playwright/pr-64/artifacts/legacy-session-manifest.json b/output/playwright/pr-64/artifacts/legacy-session-manifest.json new file mode 100644 index 0000000..16499e8 --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-session-manifest.json @@ -0,0 +1,518 @@ +{ + "schemaVersion": 1, + "sessionId": "fe170256-a52e-497e-8502-5e589fcb7bea", + "state": "completed", + "createdAt": "2026-08-28T07:50:35.521Z", + "target": { + "id": "node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "title": "Node Network Devtools (Legacy)", + "type": "node", + "url": "file:///node_modules/node-network-devtools/dist/fork", + "webSocketDebuggerUrl": "ws://127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68", + "discoveryUrl": "http://127.0.0.1:59430/json/list" + }, + "files": { + "events": "events.ndjson", + "bodies": "bodies" + }, + "completedAt": "2026-08-28T07:54:18.050Z", + "stats": { + "eventCount": 54, + "requestCount": 12, + "bodyCount": 10, + "bodyErrorCount": 0, + "failedRequestCount": 1 + }, + "requestIndex": { + "0cc5f65a-40ac-4130-9be9-3f41d81ff929": { + "requestId": "0cc5f65a-40ac-4130-9be9-3f41d81ff929", + "firstSequence": 2, + "requestTimestamp": 0.009, + "wallTime": 1787903435.508, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59430/json/list", + "method": "GET", + "headers": {}, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 0.017, + "response": { + "url": "http://127.0.0.1:59430/json/list", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-origin": "*", + "cache-control": "no-store", + "connection": "keep-alive", + "content-type": "application/json; charset=utf-8", + "date": "Fri, 28 Aug 2026 07:50:35 GMT", + "keep-alive": "timeout=5", + "transfer-encoding": "chunked" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 0.018, + "encodedDataLength": 821 + }, + "9405f647-edce-4fb1-a015-9e7b89878de5": { + "requestId": "9405f647-edce-4fb1-a015-9e7b89878de5", + "firstSequence": 3, + "requestTimestamp": 0.011, + "wallTime": 1787903435.512, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59430/json/version", + "method": "GET", + "headers": {}, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 0.019, + "response": { + "url": "http://127.0.0.1:59430/json/version", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-origin": "*", + "cache-control": "no-store", + "connection": "keep-alive", + "content-type": "application/json; charset=utf-8", + "date": "Fri, 28 Aug 2026 07:50:35 GMT", + "keep-alive": "timeout=5", + "transfer-encoding": "chunked" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 0.019, + "encodedDataLength": 248 + }, + "9e61ccb6-be11-43f9-8355-2d64a4cb6e60": { + "requestId": "9e61ccb6-be11-43f9-8355-2d64a4cb6e60", + "firstSequence": 4, + "requestTimestamp": 0.011, + "wallTime": 1787903435.512, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59430/json/protocol", + "method": "GET", + "headers": {}, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 0.019, + "response": { + "url": "http://127.0.0.1:59430/json/protocol", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-origin": "*", + "cache-control": "no-store", + "connection": "keep-alive", + "content-type": "application/json; charset=utf-8", + "date": "Fri, 28 Aug 2026 07:50:35 GMT", + "keep-alive": "timeout=5", + "transfer-encoding": "chunked" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 0.019, + "encodedDataLength": 1323 + }, + "58134218-34a0-4b54-a2ab-e1fd65450177": { + "requestId": "58134218-34a0-4b54-a2ab-e1fd65450177", + "firstSequence": 15, + "requestTimestamp": 79.291, + "wallTime": 1787903514.791, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59428/get?token=legacy-http-get-01", + "method": "GET", + "headers": { + "host": "127.0.0.1:59428" + }, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 79.292, + "response": { + "url": "http://127.0.0.1:59428/get?token=legacy-http-get-01", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/plain; charset=utf-8", + "content-length": "38", + "x-manual-case": "legacy-http-get-01", + "date": "Fri, 28 Aug 2026 07:51:54 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "text/plain" + }, + "finishedTimestamp": 79.293, + "encodedDataLength": 38 + }, + "679ed0e5-ef59-4d2e-89ab-391343c82c31": { + "requestId": "679ed0e5-ef59-4d2e-89ab-391343c82c31", + "firstSequence": 19, + "requestTimestamp": 83.251, + "wallTime": 1787903518.749, + "resourceType": "Other", + "request": { + "url": "https://127.0.0.1:59429/secure-get?token=legacy-https-get-02", + "method": "GET", + "headers": { + "host": "127.0.0.1:59429" + }, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 83.257, + "response": { + "url": "https://127.0.0.1:59429/secure-get?token=legacy-https-get-02", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/plain; charset=utf-8", + "content-length": "41", + "x-manual-case": "legacy-https-get-02", + "date": "Fri, 28 Aug 2026 07:51:58 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "text/plain" + }, + "finishedTimestamp": 83.257, + "encodedDataLength": 41 + }, + "3e47e0b7-987b-463c-aa0e-9765fc6d7e11": { + "requestId": "3e47e0b7-987b-463c-aa0e-9765fc6d7e11", + "firstSequence": 23, + "requestTimestamp": 87.391, + "wallTime": 1787903522.892, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59428/post?token=legacy-fetch-post-03", + "method": "POST", + "headers": { + "content-type": "text/plain; charset=utf-8", + "x-manual-case": "legacy-fetch-post-03" + }, + "initialPriority": "High", + "mixedContentType": "none", + "postData": "manual-request-body:legacy-fetch-post-03", + "hasPostData": true + }, + "responseTimestamp": 87.396, + "response": { + "url": "http://127.0.0.1:59428/post?token=legacy-fetch-post-03", + "status": 201, + "statusText": "Created", + "headers": { + "connection": "keep-alive", + "content-length": "89", + "content-type": "application/json; charset=utf-8", + "date": "Fri, 28 Aug 2026 07:52:02 GMT", + "keep-alive": "timeout=5", + "x-manual-case": "legacy-fetch-post-03" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 87.397, + "encodedDataLength": 89 + }, + "1b1612b8-6e03-4483-8d4f-74c232a36835": { + "requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "firstSequence": 27, + "requestTimestamp": 91.466, + "wallTime": 1787903526.966, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59428/trace?token=legacy-trace-04", + "method": "GET", + "headers": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64", + "host": "127.0.0.1:59428" + }, + "initialPriority": "High", + "mixedContentType": "none" + }, + "trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + }, + "responseTimestamp": 91.468, + "response": { + "url": "http://127.0.0.1:59428/trace?token=legacy-trace-04", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "133", + "date": "Fri, 28 Aug 2026 07:52:06 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 91.468, + "encodedDataLength": 133 + }, + "25379421-e6bd-43f0-92a2-eb9f2345a4ac": { + "requestId": "25379421-e6bd-43f0-92a2-eb9f2345a4ac", + "firstSequence": 31, + "requestTimestamp": 95.432, + "wallTime": 1787903530.932, + "resourceType": "Fetch", + "request": { + "url": "http://127.0.0.1:59428/reset?token=legacy-failed-05", + "method": "GET", + "headers": { + "host": "127.0.0.1:59428" + }, + "initialPriority": "High", + "mixedContentType": "none" + }, + "finishedTimestamp": 95.433, + "failure": { + "errorText": "socket hang up", + "canceled": false + } + }, + "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe": { + "requestId": "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe", + "firstSequence": 33, + "requestTimestamp": 99.432, + "wallTime": 1787903534.932, + "resourceType": "EventSource", + "request": { + "url": "http://127.0.0.1:59428/sse?token=legacy-sse-06", + "method": "GET", + "headers": {}, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 99.435, + "response": { + "url": "http://127.0.0.1:59428/sse?token=legacy-sse-06", + "status": 200, + "statusText": "OK", + "headers": { + "cache-control": "no-cache", + "connection": "keep-alive", + "content-length": "81", + "content-type": "text/event-stream; charset=utf-8", + "date": "Fri, 28 Aug 2026 07:52:14 GMT", + "keep-alive": "timeout=5" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "text/event-stream" + }, + "finishedTimestamp": 99.435, + "encodedDataLength": 81 + }, + "ef2b1aa4-f871-4c37-a398-91a29ed42bd8": { + "requestId": "ef2b1aa4-f871-4c37-a398-91a29ed42bd8", + "firstSequence": 39 + }, + "97ab50a1-7dc0-4698-8eb6-6c585c1b0800": { + "requestId": "97ab50a1-7dc0-4698-8eb6-6c585c1b0800", + "firstSequence": 47, + "requestTimestamp": 107.491, + "wallTime": 1787903542.991, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08", + "method": "GET", + "headers": { + "host": "127.0.0.1:59428" + }, + "initialPriority": "High", + "mixedContentType": "none" + }, + "responseTimestamp": 107.491, + "response": { + "url": "http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08", + "status": 207, + "statusText": "Manual Mock HTTP", + "headers": { + "content-type": "application/json; charset=utf-8", + "x-nnd-mock": "http", + "content-length": "67" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 107.491, + "encodedDataLength": 67 + }, + "a85e4f7f-a238-44c7-866e-2b7cfb325da1": { + "requestId": "a85e4f7f-a238-44c7-866e-2b7cfb325da1", + "firstSequence": 51, + "requestTimestamp": 111.49, + "wallTime": 1787903546.991, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09", + "method": "POST", + "headers": { + "content-type": "text/plain", + "x-manual-mock": "fetch" + }, + "initialPriority": "High", + "mixedContentType": "none", + "postData": "must-not-reach-origin:legacy-mock-fetch-09", + "hasPostData": true + }, + "responseTimestamp": 111.491, + "response": { + "url": "http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09", + "status": 202, + "statusText": "Manual Mock Fetch", + "headers": { + "content-type": "application/json; charset=utf-8", + "x-nnd-mock": "fetch" + }, + "connectionReused": false, + "encodedDataLength": 0, + "charset": "utf-8", + "mimeType": "application/json" + }, + "finishedTimestamp": 111.491, + "encodedDataLength": 68 + } + }, + "bodyIndex": { + "0cc5f65a-40ac-4130-9be9-3f41d81ff929": { + "requestId": "0cc5f65a-40ac-4130-9be9-3f41d81ff929", + "path": "bodies/9d4da820dd90637b-4d42de0bb5b2269011ec887a81c45783270bc1fa034d2b16913b29a466eed86d.body", + "sha256": "4d42de0bb5b2269011ec887a81c45783270bc1fa034d2b16913b29a466eed86d", + "byteLength": 821, + "base64Encoded": false, + "mimeType": "application/json" + }, + "9405f647-edce-4fb1-a015-9e7b89878de5": { + "requestId": "9405f647-edce-4fb1-a015-9e7b89878de5", + "path": "bodies/856a19bcfdf19a59-bbf42bcaa1d02652fd51fb30e5a96e01da3b85f015fd1635b2adcee547d08444.body", + "sha256": "bbf42bcaa1d02652fd51fb30e5a96e01da3b85f015fd1635b2adcee547d08444", + "byteLength": 248, + "base64Encoded": false, + "mimeType": "application/json" + }, + "9e61ccb6-be11-43f9-8355-2d64a4cb6e60": { + "requestId": "9e61ccb6-be11-43f9-8355-2d64a4cb6e60", + "path": "bodies/1e5129332eeffbac-59b00c761bb938bbd60117eeafca6eb93ed4c9df481b492b37cd14fbcb1d121a.body", + "sha256": "59b00c761bb938bbd60117eeafca6eb93ed4c9df481b492b37cd14fbcb1d121a", + "byteLength": 1323, + "base64Encoded": false, + "mimeType": "application/json" + }, + "58134218-34a0-4b54-a2ab-e1fd65450177": { + "requestId": "58134218-34a0-4b54-a2ab-e1fd65450177", + "path": "bodies/2d08ba5699d46627-4dbbb1b839dde1985bb99f2e0191e702c723ed9c0270c8863fa7a15b1a4d1241.body", + "sha256": "4dbbb1b839dde1985bb99f2e0191e702c723ed9c0270c8863fa7a15b1a4d1241", + "byteLength": 38, + "base64Encoded": false, + "mimeType": "text/plain" + }, + "679ed0e5-ef59-4d2e-89ab-391343c82c31": { + "requestId": "679ed0e5-ef59-4d2e-89ab-391343c82c31", + "path": "bodies/dc8bd877cc1f50c9-3da027641befa9a07e036caa733aff0e6bbdbe2d79bd51592a2b98af03823010.body", + "sha256": "3da027641befa9a07e036caa733aff0e6bbdbe2d79bd51592a2b98af03823010", + "byteLength": 41, + "base64Encoded": false, + "mimeType": "text/plain" + }, + "3e47e0b7-987b-463c-aa0e-9765fc6d7e11": { + "requestId": "3e47e0b7-987b-463c-aa0e-9765fc6d7e11", + "path": "bodies/d520b6573b0f1805-06a511ade7e9e2ec749546b3c19cfa3ad033741a2431101b2b8d5c4c8f773686.body", + "sha256": "06a511ade7e9e2ec749546b3c19cfa3ad033741a2431101b2b8d5c4c8f773686", + "byteLength": 89, + "base64Encoded": false, + "mimeType": "application/json" + }, + "1b1612b8-6e03-4483-8d4f-74c232a36835": { + "requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "path": "bodies/d0b35c18e5a6e6fa-7ea97b7efb1716877e99643b9309fab128cfc9b4361865629e7355ff2a619adf.body", + "sha256": "7ea97b7efb1716877e99643b9309fab128cfc9b4361865629e7355ff2a619adf", + "byteLength": 133, + "base64Encoded": false, + "mimeType": "application/json" + }, + "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe": { + "requestId": "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe", + "path": "bodies/285b774da6e19996-099ad30fee6432ff6c49a303917619cf899ffa89ce05a26d87a5e118bea4cae0.body", + "sha256": "099ad30fee6432ff6c49a303917619cf899ffa89ce05a26d87a5e118bea4cae0", + "byteLength": 81, + "base64Encoded": false, + "mimeType": "text/event-stream" + }, + "97ab50a1-7dc0-4698-8eb6-6c585c1b0800": { + "requestId": "97ab50a1-7dc0-4698-8eb6-6c585c1b0800", + "path": "bodies/8ad3dde286c71e26-bf55b18bcb5c10539e5529e2f80c8bea6021a6ec17d157d21be2c3ec53c46a11.body", + "sha256": "bf55b18bcb5c10539e5529e2f80c8bea6021a6ec17d157d21be2c3ec53c46a11", + "byteLength": 67, + "base64Encoded": false, + "mimeType": "application/json" + }, + "a85e4f7f-a238-44c7-866e-2b7cfb325da1": { + "requestId": "a85e4f7f-a238-44c7-866e-2b7cfb325da1", + "path": "bodies/5e5ac978a5a357db-2529fff188d8b259ca882f4c4ab063d29f11b43e18b2ed57718fea224673ddda.body", + "sha256": "2529fff188d8b259ca882f4c4ab063d29f11b43e18b2ed57718fea224673ddda", + "byteLength": 68, + "base64Encoded": false, + "mimeType": "application/json" + } + }, + "traceIndex": { + "4bf92f3577b34da6a3ce929d0e0e4736": { + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "requestIds": ["1b1612b8-6e03-4483-8d4f-74c232a36835"], + "spans": [ + { + "requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64" + } + ] + } + }, + "issues": [] +} diff --git a/output/playwright/pr-64/artifacts/legacy-session.har b/output/playwright/pr-64/artifacts/legacy-session.har new file mode 100644 index 0000000..8deee68 --- /dev/null +++ b/output/playwright/pr-64/artifacts/legacy-session.har @@ -0,0 +1,832 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2026-08-28T07:50:35.508Z", + "time": 9, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59430/json/list", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cache-control", + "value": "no-store" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:50:35 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "content": { + "size": 821, + "mimeType": "application/json", + "text": "[{\"id\":\"node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68\",\"title\":\"Node Network Devtools (Legacy)\",\"type\":\"node\",\"url\":\"file:///node_modules/node-network-devtools/dist/fork\",\"webSocketDebuggerUrl\":\"ws://127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68\",\"devtoolsFrontendUrl\":\"devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68\",\"devtoolsFrontendUrlCompat\":\"devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68\",\"discoveryUrl\":\"http://127.0.0.1:59430/json/list\"}]" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 821 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 8.000000000000002, + "receive": 0.9999999999999974, + "ssl": -1 + }, + "_requestId": "0cc5f65a-40ac-4130-9be9-3f41d81ff929" + }, + { + "startedDateTime": "2026-08-28T07:50:35.512Z", + "time": 8, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59430/json/version", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cache-control", + "value": "no-store" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:50:35 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "content": { + "size": 248, + "mimeType": "application/json", + "text": "{\"Browser\":\"node-network-devtools/2\",\"Protocol-Version\":\"1.3\",\"User-Agent\":\"Node.js/v24.16.0\",\"V8-Version\":\"13.6.233.17-node.49\",\"webSocketDebuggerUrl\":\"ws://127.0.0.1:59430/devtools/page/node-network-devtools-b4efbb3a-70c3-435c-a554-567e33524f68\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 248 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 8, + "receive": 0, + "ssl": -1 + }, + "_requestId": "9405f647-edce-4fb1-a015-9e7b89878de5" + }, + { + "startedDateTime": "2026-08-28T07:50:35.512Z", + "time": 8, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59430/json/protocol", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cache-control", + "value": "no-store" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:50:35 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + }, + { + "name": "transfer-encoding", + "value": "chunked" + } + ], + "content": { + "size": 1323, + "mimeType": "application/json", + "text": "{\"version\":{\"major\":\"1\",\"minor\":\"3\"},\"domains\":[{\"domain\":\"Network\",\"version\":\"1.3\",\"commands\":[{\"name\":\"enable\"},{\"name\":\"disable\"},{\"name\":\"setAttachDebugStack\"},{\"name\":\"emulateNetworkConditionsByRule\",\"returns\":[{\"name\":\"ruleIds\",\"type\":\"array\",\"items\":{\"type\":\"string\"}}]},{\"name\":\"overrideNetworkState\"},{\"name\":\"clearAcceptedEncodingsOverride\"},{\"name\":\"getResponseBody\",\"parameters\":[{\"name\":\"requestId\",\"type\":\"string\"}],\"returns\":[{\"name\":\"body\",\"type\":\"string\"},{\"name\":\"base64Encoded\",\"type\":\"boolean\"}]}],\"events\":[{\"name\":\"requestWillBeSent\"},{\"name\":\"responseReceived\"},{\"name\":\"dataReceived\"},{\"name\":\"loadingFinished\"},{\"name\":\"loadingFailed\"},{\"name\":\"eventSourceMessageReceived\"},{\"name\":\"webSocketCreated\"},{\"name\":\"webSocketWillSendHandshakeRequest\"},{\"name\":\"webSocketHandshakeResponseReceived\"},{\"name\":\"webSocketFrameSent\"},{\"name\":\"webSocketFrameReceived\"},{\"name\":\"webSocketClosed\"}]},{\"domain\":\"Debugger\",\"version\":\"1.3\",\"commands\":[{\"name\":\"enable\"},{\"name\":\"disable\"},{\"name\":\"getScriptSource\",\"parameters\":[{\"name\":\"scriptId\",\"type\":\"string\"}],\"returns\":[{\"name\":\"scriptSource\",\"type\":\"string\"}]}],\"events\":[{\"name\":\"scriptParsed\"}]},{\"domain\":\"Runtime\",\"version\":\"1.3\",\"commands\":[{\"name\":\"enable\"},{\"name\":\"disable\"}]},{\"domain\":\"Schema\",\"version\":\"1.3\",\"commands\":[{\"name\":\"getDomains\"}]}]}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 1323 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 8, + "receive": 0, + "ssl": -1 + }, + "_requestId": "9e61ccb6-be11-43f9-8355-2d64a4cb6e60" + }, + { + "startedDateTime": "2026-08-28T07:51:54.791Z", + "time": 2.0000000000095497, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/get?token=legacy-http-get-01", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-http-get-01" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "legacy-http-get-01" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:51:54 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:legacy-http-get-01" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.0000000000047748, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "58134218-34a0-4b54-a2ab-e1fd65450177" + }, + { + "startedDateTime": "2026-08-28T07:51:58.749Z", + "time": 6.000000000000227, + "request": { + "method": "GET", + "url": "https://127.0.0.1:59429/secure-get?token=legacy-https-get-02", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59429" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-https-get-02" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "41" + }, + { + "name": "x-manual-case", + "value": "legacy-https-get-02" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:51:58 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 41, + "mimeType": "text/plain", + "text": "manual-https-response:legacy-https-get-02" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 41 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 6.000000000000227, + "receive": 0, + "ssl": -1 + }, + "_requestId": "679ed0e5-ef59-4d2e-89ab-391343c82c31" + }, + { + "startedDateTime": "2026-08-28T07:52:02.892Z", + "time": 6.000000000000227, + "request": { + "method": "POST", + "url": "http://127.0.0.1:59428/post?token=legacy-fetch-post-03", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "x-manual-case", + "value": "legacy-fetch-post-03" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-fetch-post-03" + } + ], + "postData": { + "mimeType": "text/plain; charset=utf-8", + "text": "manual-request-body:legacy-fetch-post-03" + }, + "headersSize": -1, + "bodySize": 40 + }, + "response": { + "status": 201, + "statusText": "Created", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "89" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:02 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + }, + { + "name": "x-manual-case", + "value": "legacy-fetch-post-03" + } + ], + "content": { + "size": 89, + "mimeType": "application/json", + "text": "{\"token\":\"legacy-fetch-post-03\",\"requestBody\":\"manual-request-body:legacy-fetch-post-03\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 89 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 4.9999999999954525, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "3e47e0b7-987b-463c-aa0e-9765fc6d7e11" + }, + { + "startedDateTime": "2026-08-28T07:52:06.966Z", + "time": 2.0000000000095497, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/trace?token=legacy-trace-04", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "traceparent", + "value": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + }, + { + "name": "tracestate", + "value": "vendor=manual-pr64" + }, + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-trace-04" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "133" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:06 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 133, + "mimeType": "application/json", + "text": "{\"token\":\"legacy-trace-04\",\"traceparent\":\"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\",\"tracestate\":\"vendor=manual-pr64\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 133 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.0000000000095497, + "receive": 0, + "ssl": -1 + }, + "_requestId": "1b1612b8-6e03-4483-8d4f-74c232a36835", + "_trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + } + }, + { + "startedDateTime": "2026-08-28T07:52:10.932Z", + "time": 1.0000000000047748, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/reset?token=legacy-failed-05", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-failed-05" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "socket hang up", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "content": { + "size": 0, + "mimeType": "application/octet-stream" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "25379421-e6bd-43f0-92a2-eb9f2345a4ac", + "_failure": { + "errorText": "socket hang up", + "canceled": false + } + }, + { + "startedDateTime": "2026-08-28T07:52:14.932Z", + "time": 3.0000000000001137, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/sse?token=legacy-sse-06", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [ + { + "name": "token", + "value": "legacy-sse-06" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "content-length", + "value": "81" + }, + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:52:14 GMT" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 81, + "mimeType": "text/event-stream", + "text": "id: 1\nevent: manual\ndata: sse-legacy-sse-06\n\nid: 2\ndata: complete-legacy-sse-06\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 81 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 3.0000000000001137, + "receive": 0, + "ssl": -1 + }, + "_requestId": "21870c2f-ae5c-45b6-a61b-f2827cf4b8fe" + }, + { + "startedDateTime": "2026-08-28T07:50:35.521Z", + "time": 0, + "request": { + "method": "GET", + "url": "", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "content": { + "size": 0, + "mimeType": "application/octet-stream" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "ef2b1aa4-f871-4c37-a398-91a29ed42bd8" + }, + { + "startedDateTime": "2026-08-28T07:52:22.991Z", + "time": 0, + "request": { + "method": "GET", + "url": "http://127.0.0.1:59428/mock-http?token=legacy-mock-http-08", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:59428" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-mock-http-08" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 207, + "statusText": "Manual Mock HTTP", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "x-nnd-mock", + "value": "http" + }, + { + "name": "content-length", + "value": "67" + } + ], + "content": { + "size": 67, + "mimeType": "application/json", + "text": "{\"mocked\":true,\"transport\":\"http\",\"source\":\"node-network-devtools\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 67 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "97ab50a1-7dc0-4698-8eb6-6c585c1b0800" + }, + { + "startedDateTime": "2026-08-28T07:52:26.991Z", + "time": 1.0000000000047748, + "request": { + "method": "POST", + "url": "http://127.0.0.1:59428/mock-fetch?token=legacy-mock-fetch-09", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain" + }, + { + "name": "x-manual-mock", + "value": "fetch" + } + ], + "queryString": [ + { + "name": "token", + "value": "legacy-mock-fetch-09" + } + ], + "postData": { + "mimeType": "text/plain", + "text": "must-not-reach-origin:legacy-mock-fetch-09" + }, + "headersSize": -1, + "bodySize": 42 + }, + "response": { + "status": 202, + "statusText": "Manual Mock Fetch", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "x-nnd-mock", + "value": "fetch" + } + ], + "content": { + "size": 68, + "mimeType": "application/json", + "text": "{\"mocked\":true,\"transport\":\"fetch\",\"source\":\"node-network-devtools\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 68 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.0000000000047748, + "receive": 0, + "ssl": -1 + }, + "_requestId": "a85e4f7f-a238-44c7-866e-2b7cfb325da1" + } + ] + } +} diff --git a/output/playwright/pr-64/artifacts/native-dispose-summary.json b/output/playwright/pr-64/artifacts/native-dispose-summary.json new file mode 100644 index 0000000..7f17c58 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-dispose-summary.json @@ -0,0 +1,5 @@ +{ + "registrationState": "disposed", + "discoveryClosed": true, + "targetSocketClosed": true +} diff --git a/output/playwright/pr-64/artifacts/native-events.ndjson b/output/playwright/pr-64/artifacts/native-events.ndjson new file mode 100644 index 0000000..c0258bc --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-events.ndjson @@ -0,0 +1,24 @@ +{"schemaVersion":1,"sequence":1,"recordedAt":"2026-08-28T07:47:55.120Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-1","request":{"url":"http://127.0.0.1:58925/get?token=native-http-get-01","method":"GET","headers":{"host":"127.0.0.1:58925"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestCreated","scriptId":"369","url":"node:internal/inspector/network_http","lineNumber":81,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"ClientRequest","scriptId":"149","url":"node:_http_client","lineNumber":442,"columnNumber":34},{"functionName":"request","scriptId":"146","url":"node:http","lineNumber":107,"columnNumber":9},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":113,"columnNumber":44},{"functionName":"httpRequest","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":111,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":493,"columnNumber":21},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":86.3044,"wallTime":1787903275119}} +{"schemaVersion":1,"sequence":2,"recordedAt":"2026-08-28T07:47:55.124Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-1","timestamp":86.3068,"type":"Other","response":{"url":"http://127.0.0.1:58925/get?token=native-http-get-01","status":200,"statusText":"OK","headers":{"content-type":"text/plain; charset=utf-8","content-length":"38","x-manual-case":"native-http-get-01","date":"Fri, 28 Aug 2026 07:47:55 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"mimeType":"text/plain","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":3,"recordedAt":"2026-08-28T07:47:55.126Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-1","timestamp":86.3071}} +{"schemaVersion":1,"sequence":4,"recordedAt":"2026-08-28T07:47:59.053Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-2","request":{"url":"https://127.0.0.1:58926/secure-get?token=native-https-get-02","method":"GET","headers":{"host":"127.0.0.1:58926"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestCreated","scriptId":"369","url":"node:internal/inspector/network_http","lineNumber":81,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"ClientRequest","scriptId":"149","url":"node:_http_client","lineNumber":442,"columnNumber":34},{"functionName":"request","scriptId":"194","url":"node:https","lineNumber":628,"columnNumber":9},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":113,"columnNumber":44},{"functionName":"httpRequest","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":111,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":496,"columnNumber":21},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":90.2372,"wallTime":1787903279052}} +{"schemaVersion":1,"sequence":5,"recordedAt":"2026-08-28T07:47:59.063Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-2","timestamp":90.245,"type":"Other","response":{"url":"https://127.0.0.1:58926/secure-get?token=native-https-get-02","status":200,"statusText":"OK","headers":{"content-type":"text/plain; charset=utf-8","content-length":"41","x-manual-case":"native-https-get-02","date":"Fri, 28 Aug 2026 07:47:59 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"mimeType":"text/plain","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":6,"recordedAt":"2026-08-28T07:47:59.065Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-2","timestamp":90.2455}} +{"schemaVersion":1,"sequence":7,"recordedAt":"2026-08-28T07:48:03.102Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-3","request":{"url":"http://127.0.0.1:58925/post?token=native-fetch-post-03","method":"POST","headers":{"content-type":"text/plain; charset=utf-8","x-manual-case":"native-fetch-post-03","accept":"*/*","accept-language":"*","sec-fetch-mode":"cors","user-agent":"node","accept-encoding":"gzip, deflate"},"hasPostData":true},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestStart","scriptId":"372","url":"node:internal/inspector/network_undici","lineNumber":77,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"Request","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2991,"columnNumber":26},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9231,"columnNumber":24},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2563,"columnNumber":31},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9705,"columnNumber":26},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":54},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":15},{"functionName":"httpNetworkFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13536,"columnNumber":72},{"functionName":"httpNetworkOrCacheFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13406,"columnNumber":38},{"functionName":"httpFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13228,"columnNumber":42},{"functionName":"schemeFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13145,"columnNumber":17},{"functionName":"mainFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12989,"columnNumber":29},{"functionName":"fetching","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12958,"columnNumber":6},{"functionName":"fetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12822,"columnNumber":19},{"functionName":"fetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":17455,"columnNumber":9},{"functionName":"fetch","scriptId":"64","url":"node:internal/bootstrap/web/exposed-window-or-worker","lineNumber":82,"columnNumber":11},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":501,"columnNumber":29},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":94.2859,"wallTime":1787903283101}} +{"schemaVersion":1,"sequence":8,"recordedAt":"2026-08-28T07:48:03.114Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-3","timestamp":94.2956,"type":"Fetch","response":{"url":"http://127.0.0.1:58925/post?token=native-fetch-post-03","status":201,"statusText":"Created","headers":{"content-type":"application/json; charset=utf-8","content-length":"89","x-manual-case":"native-fetch-post-03","Date":"Fri, 28 Aug 2026 07:48:03 GMT","Connection":"keep-alive","Keep-Alive":"timeout=5"},"mimeType":"application/json","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":9,"recordedAt":"2026-08-28T07:48:03.115Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-3","timestamp":94.296}} +{"schemaVersion":1,"sequence":10,"recordedAt":"2026-08-28T07:48:07.067Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-4","request":{"url":"http://127.0.0.1:58925/trace?token=native-trace-04","method":"GET","headers":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01","tracestate":"vendor=manual-pr64","host":"127.0.0.1:58925"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestCreated","scriptId":"369","url":"node:internal/inspector/network_http","lineNumber":81,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"ClientRequest","scriptId":"149","url":"node:_http_client","lineNumber":442,"columnNumber":34},{"functionName":"request","scriptId":"146","url":"node:http","lineNumber":107,"columnNumber":9},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":113,"columnNumber":44},{"functionName":"httpRequest","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":111,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":537,"columnNumber":21},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":98.2512,"wallTime":1787903287066}} +{"schemaVersion":1,"sequence":11,"recordedAt":"2026-08-28T07:48:07.071Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-4","timestamp":98.2539,"type":"Other","response":{"url":"http://127.0.0.1:58925/trace?token=native-trace-04","status":200,"statusText":"OK","headers":{"content-type":"application/json; charset=utf-8","content-length":"133","date":"Fri, 28 Aug 2026 07:48:07 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"mimeType":"application/json","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":12,"recordedAt":"2026-08-28T07:48:07.072Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-4","timestamp":98.2542}} +{"schemaVersion":1,"sequence":13,"recordedAt":"2026-08-28T07:48:11.025Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-5","request":{"url":"http://127.0.0.1:58925/reset?token=native-failed-05","method":"GET","headers":{"host":"127.0.0.1:58925"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestCreated","scriptId":"369","url":"node:internal/inspector/network_http","lineNumber":81,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"ClientRequest","scriptId":"149","url":"node:_http_client","lineNumber":442,"columnNumber":34},{"functionName":"request","scriptId":"146","url":"node:http","lineNumber":107,"columnNumber":9},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":113,"columnNumber":44},{"functionName":"httpRequest","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":111,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":545,"columnNumber":14},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":102.21,"wallTime":1787903291025}} +{"schemaVersion":1,"sequence":14,"recordedAt":"2026-08-28T07:48:11.028Z","method":"Network.loadingFailed","params":{"requestId":"node-network-event-5","timestamp":102.212,"type":"Other","errorText":"socket hang up"}} +{"schemaVersion":1,"sequence":15,"recordedAt":"2026-08-28T07:48:15.010Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-6","request":{"url":"http://127.0.0.1:58925/sse?token=native-sse-06","method":"GET","headers":{"accept":"*/*","accept-language":"*","sec-fetch-mode":"cors","user-agent":"node","accept-encoding":"gzip, deflate"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestStart","scriptId":"372","url":"node:internal/inspector/network_undici","lineNumber":77,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"Request","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2991,"columnNumber":26},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9231,"columnNumber":24},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2563,"columnNumber":31},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9705,"columnNumber":26},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":54},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":15},{"functionName":"httpNetworkFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13536,"columnNumber":72},{"functionName":"httpNetworkOrCacheFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13406,"columnNumber":38},{"functionName":"httpFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13228,"columnNumber":42},{"functionName":"schemeFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13145,"columnNumber":17},{"functionName":"mainFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12989,"columnNumber":29},{"functionName":"fetching","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12958,"columnNumber":6},{"functionName":"fetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12822,"columnNumber":19},{"functionName":"fetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":17455,"columnNumber":9},{"functionName":"fetch","scriptId":"64","url":"node:internal/bootstrap/web/exposed-window-or-worker","lineNumber":82,"columnNumber":11},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":558,"columnNumber":29},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":106.193,"wallTime":1787903295008}} +{"schemaVersion":1,"sequence":16,"recordedAt":"2026-08-28T07:48:15.015Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-6","timestamp":106.198,"type":"Fetch","response":{"url":"http://127.0.0.1:58925/sse?token=native-sse-06","status":200,"statusText":"OK","headers":{"content-type":"text/event-stream; charset=utf-8","content-length":"81","cache-control":"no-cache","Date":"Fri, 28 Aug 2026 07:48:15 GMT","Connection":"keep-alive","Keep-Alive":"timeout=5"},"mimeType":"text/event-stream","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":17,"recordedAt":"2026-08-28T07:48:15.017Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-6","timestamp":106.198}} +{"schemaVersion":1,"sequence":18,"recordedAt":"2026-08-28T07:48:18.968Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-7","request":{"url":"http://127.0.0.1:58925/websocket?token=native-websocket-07","method":"GET","headers":{"sec-websocket-key":"iVtSkmOz5zD/H90P5l+npg==","sec-websocket-version":"13","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","accept":"*/*","accept-language":"*","sec-fetch-mode":"websocket","user-agent":"node","pragma":"no-cache","cache-control":"no-cache","accept-encoding":"gzip, deflate"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestStart","scriptId":"372","url":"node:internal/inspector/network_undici","lineNumber":77,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"Request","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2991,"columnNumber":26},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9231,"columnNumber":24},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2563,"columnNumber":31},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"[dispatch]","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":9705,"columnNumber":26},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":2292,"columnNumber":32},{"functionName":"","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":54},{"functionName":"dispatch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13638,"columnNumber":15},{"functionName":"httpNetworkFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13536,"columnNumber":72},{"functionName":"httpNetworkOrCacheFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13406,"columnNumber":38},{"functionName":"httpFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13228,"columnNumber":42},{"functionName":"schemeFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13145,"columnNumber":17},{"functionName":"mainFetch","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12989,"columnNumber":29},{"functionName":"fetching","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":12958,"columnNumber":6},{"functionName":"establishWebSocketConnection","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":14503,"columnNumber":25},{"functionName":"WebSocket","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":15253,"columnNumber":35},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":159,"columnNumber":19},{"functionName":"nativeWebSocketRoundTrip","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":158,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":564,"columnNumber":91},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":110.152,"wallTime":1787903298967}} +{"schemaVersion":1,"sequence":19,"recordedAt":"2026-08-28T07:48:18.977Z","method":"Network.webSocketCreated","params":{"requestId":"node-network-event-8","url":"ws://127.0.0.1:58925/websocket?token=native-websocket-07","initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"webSocketCreated","scriptId":"302","url":"node:inspector","lineNumber":221,"columnNumber":32},{"functionName":"onWebSocketOpen","scriptId":"372","url":"node:internal/inspector/network_undici","lineNumber":214,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"#onConnectionEstablished","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":15441,"columnNumber":24},{"functionName":"onConnectionEstablished","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":15185,"columnNumber":111},{"functionName":"processResponse","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":14561,"columnNumber":18},{"functionName":"","scriptId":"155","url":"node:internal/deps/undici/undici","lineNumber":13203,"columnNumber":22},{"functionName":"","scriptId":"32","url":"node:internal/process/task_queues","lineNumber":150,"columnNumber":6},{"functionName":"runInAsyncScope","scriptId":"35","url":"node:async_hooks","lineNumber":226,"columnNumber":13},{"functionName":"runMicrotask","scriptId":"32","url":"node:internal/process/task_queues","lineNumber":147,"columnNumber":7},{"functionName":"processTicksAndRejections","scriptId":"32","url":"node:internal/process/task_queues","lineNumber":103,"columnNumber":4}]}}}} +{"schemaVersion":1,"sequence":20,"recordedAt":"2026-08-28T07:48:18.979Z","method":"Network.webSocketHandshakeResponseReceived","params":{"requestId":"node-network-event-8","timestamp":110.157,"response":{"status":101,"statusText":"Switching Protocols","headers":{"upgrade":"websocket","connection":"Upgrade","sec-websocket-accept":"QwNMRFhpyLgDRWoVQNFiDz4Z3Wg="}}}} +{"schemaVersion":1,"sequence":21,"recordedAt":"2026-08-28T07:48:18.980Z","method":"Network.webSocketClosed","params":{"requestId":"node-network-event-8","timestamp":110.163}} +{"schemaVersion":1,"sequence":22,"recordedAt":"2026-08-28T07:49:35.675Z","method":"Network.requestWillBeSent","params":{"requestId":"node-network-event-9","request":{"url":"http://127.0.0.1:58925/get?token=native-http-get-08","method":"GET","headers":{"host":"127.0.0.1:58925"},"hasPostData":false},"initiator":{"type":"script","stack":{"callFrames":[{"functionName":"broadcastToFrontend","scriptId":"302","url":"node:inspector","lineNumber":211,"columnNumber":2},{"functionName":"requestWillBeSent","scriptId":"302","url":"node:inspector","lineNumber":215,"columnNumber":33},{"functionName":"onClientRequestCreated","scriptId":"369","url":"node:internal/inspector/network_http","lineNumber":81,"columnNumber":10},{"functionName":"publish","scriptId":"31","url":"node:diagnostics_channel","lineNumber":164,"columnNumber":8},{"functionName":"ClientRequest","scriptId":"149","url":"node:_http_client","lineNumber":442,"columnNumber":34},{"functionName":"request","scriptId":"146","url":"node:http","lineNumber":107,"columnNumber":9},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":113,"columnNumber":44},{"functionName":"httpRequest","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":111,"columnNumber":9},{"functionName":"runScenario","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":493,"columnNumber":21},{"functionName":"","scriptId":"87","url":"file:///manual-evidence-server.mjs","lineNumber":982,"columnNumber":40},{"functionName":"emit","scriptId":"22","url":"node:events","lineNumber":508,"columnNumber":27},{"functionName":"parserOnIncoming","scriptId":"154","url":"node:_http_server","lineNumber":1225,"columnNumber":11},{"functionName":"parserOnHeadersComplete","scriptId":"150","url":"node:_http_common","lineNumber":124,"columnNumber":16}]}},"timestamp":186.86,"wallTime":1787903375674}} +{"schemaVersion":1,"sequence":23,"recordedAt":"2026-08-28T07:49:35.679Z","method":"Network.responseReceived","params":{"requestId":"node-network-event-9","timestamp":186.862,"type":"Other","response":{"url":"http://127.0.0.1:58925/get?token=native-http-get-08","status":200,"statusText":"OK","headers":{"content-type":"text/plain; charset=utf-8","content-length":"38","x-manual-case":"native-http-get-08","date":"Fri, 28 Aug 2026 07:49:35 GMT","connection":"keep-alive","keep-alive":"timeout=5"},"mimeType":"text/plain","charset":"utf-8"}}} +{"schemaVersion":1,"sequence":24,"recordedAt":"2026-08-28T07:49:35.680Z","method":"Network.loadingFinished","params":{"requestId":"node-network-event-9","timestamp":186.863}} diff --git a/output/playwright/pr-64/artifacts/native-finalize-summary.json b/output/playwright/pr-64/artifacts/native-finalize-summary.json new file mode 100644 index 0000000..9a594b6 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-finalize-summary.json @@ -0,0 +1,90 @@ +{ + "sessionDirectory": "/.runtime/native-session-1787903188869", + "manualAssertions": { + "passed": true, + "discovery": { + "list": { + "ok": true, + "targetCount": 1, + "targetIdMatches": true + }, + "version": { + "ok": true, + "browser": "node.js/v24.16.0", + "protocolVersion": "1.1" + }, + "protocol": { + "ok": true, + "domainCount": 14, + "networkDomain": true, + "networkCommandCount": 6, + "networkEventCount": 8 + } + }, + "failedLifecycle": { + "requestId": "node-network-event-5", + "requestWillBeSent": 1, + "responseReceived": 0, + "loadingFinished": 0, + "loadingFailed": 1 + }, + "webSocketBoundary": { + "lifecycleCreated": 1, + "lifecycleClosed": 1, + "framesSent": 0, + "framesReceived": 0, + "expectedFrameCapture": false + }, + "sseBoundary": { + "requestCaptured": true, + "messageEvents": 0, + "expectedMessageCapture": false + }, + "traceBoundary": { + "explicitTraceRequests": 1, + "explicitTracePreserved": true, + "untracedBusinessRequests": 6, + "untracedHeadersAbsent": true + } + }, + "manifest": { + "schemaVersion": 1, + "state": "completed", + "stats": { + "eventCount": 24, + "requestCount": 9, + "bodyCount": 6, + "bodyErrorCount": 0, + "failedRequestCount": 1 + }, + "issues": [], + "traceContexts": [ + { + "requestId": "node-network-event-4", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64" + } + ] + }, + "har": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "entries": 9, + "statuses": [200, 200, 201, 200, 0, 200, 0, 0, 200], + "replayableEntries": 5 + }, + "replay": { + "dryRun": true, + "dryRunRequests": 5, + "dryRunPassed": true, + "realRequests": 5, + "realPassed": true + }, + "originLeakCount": 0 +} diff --git a/output/playwright/pr-64/artifacts/native-http2-probe.json b/output/playwright/pr-64/artifacts/native-http2-probe.json new file mode 100644 index 0000000..7aed7cd --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-http2-probe.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": 1, + "testedProductCommit": "449d47db89109e826eb0e7e0584777365eac3f9b", + "tarballSha256": "e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac", + "probe": "manual-native-http2-probe.mjs", + "request": { + "protocol": "h2c", + "status": 200, + "responseBody": "h2-ok", + "consumerBehavior": "request.setEncoding('utf8') plus data consumption" + }, + "cases": [ + { + "node": "v22.22.3", + "exitCode": 0, + "advertisedHttp2": true, + "eventSequence": [ + "Network.requestWillBeSent", + "Network.responseReceived", + "Network.loadingFinished" + ], + "receivedBody": "h2-ok", + "outcome": "complete lifecycle" + }, + { + "node": "v24.16.0", + "exitCode": 1, + "advertisedHttp2": false, + "eventSequence": ["Network.requestWillBeSent", "Network.responseReceived"], + "receivedBody": null, + "error": "TypeError: Missing dataLength in event", + "source": "node:internal/inspector/network_http2:222", + "outcome": "upstream Inspector crash reproduced; capability correctly withheld" + }, + { + "node": "v26.8.1", + "exitCode": 1, + "advertisedHttp2": false, + "eventSequence": ["Network.requestWillBeSent", "Network.responseReceived"], + "receivedBody": null, + "error": "TypeError: Missing dataLength in event", + "source": "node:internal/inspector/network_http2:223", + "outcome": "upstream Inspector crash reproduced; capability correctly withheld" + } + ], + "assertion": { + "status": "PASS", + "reason": "The exact package advertises HTTP/2 only on the verified Node 22.20+ 22.x line and does not inherit support into affected or future majors." + } +} diff --git a/output/playwright/pr-64/artifacts/native-replayable.har b/output/playwright/pr-64/artifacts/native-replayable.har new file mode 100644 index 0000000..82e19b3 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-replayable.har @@ -0,0 +1,440 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2026-08-28T07:47:55.119Z", + "time": 2.7000000000043656, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/get?token=native-http-get-01", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-http-get-01" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "native-http-get-01" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:47:55 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:native-http-get-01" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.3999999999944066, + "receive": 0.30000000000995897, + "ssl": -1 + }, + "_requestId": "node-network-event-1" + }, + { + "startedDateTime": "2026-08-28T07:48:03.101Z", + "time": 10.100000000008436, + "request": { + "method": "POST", + "url": "http://127.0.0.1:58925/post?token=native-fetch-post-03", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "x-manual-case", + "value": "native-fetch-post-03" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-language", + "value": "*" + }, + { + "name": "sec-fetch-mode", + "value": "cors" + }, + { + "name": "user-agent", + "value": "node" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-fetch-post-03" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 201, + "statusText": "Created", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "89" + }, + { + "name": "x-manual-case", + "value": "native-fetch-post-03" + }, + { + "name": "Date", + "value": "Fri, 28 Aug 2026 07:48:03 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Keep-Alive", + "value": "timeout=5" + } + ], + "content": { + "size": 89, + "mimeType": "application/json", + "text": "{\"token\":\"native-fetch-post-03\",\"requestBody\":\"manual-request-body:native-fetch-post-03\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 89 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 9.699999999995157, + "receive": 0.4000000000132786, + "ssl": -1 + }, + "_requestId": "node-network-event-3" + }, + { + "startedDateTime": "2026-08-28T07:48:07.066Z", + "time": 3.0000000000001137, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/trace?token=native-trace-04", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "traceparent", + "value": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + }, + { + "name": "tracestate", + "value": "vendor=manual-pr64" + }, + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-trace-04" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "133" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:48:07 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 133, + "mimeType": "application/json", + "text": "{\"token\":\"native-trace-04\",\"traceparent\":\"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\",\"tracestate\":\"vendor=manual-pr64\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 133 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.7000000000043656, + "receive": 0.2999999999957481, + "ssl": -1 + }, + "_requestId": "node-network-event-4", + "_trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + } + }, + { + "startedDateTime": "2026-08-28T07:48:15.008Z", + "time": 4.9999999999954525, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/sse?token=native-sse-06", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-language", + "value": "*" + }, + { + "name": "sec-fetch-mode", + "value": "cors" + }, + { + "name": "user-agent", + "value": "node" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-sse-06" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "content-length", + "value": "81" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "Date", + "value": "Fri, 28 Aug 2026 07:48:15 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Keep-Alive", + "value": "timeout=5" + } + ], + "content": { + "size": 81, + "mimeType": "text/event-stream", + "text": "id: 1\nevent: manual\ndata: sse-native-sse-06\n\nid: 2\ndata: complete-native-sse-06\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 81 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 4.9999999999954525, + "receive": 0, + "ssl": -1 + }, + "_requestId": "node-network-event-6" + }, + { + "startedDateTime": "2026-08-28T07:49:35.674Z", + "time": 2.999999999985903, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/get?token=native-http-get-08", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-http-get-08" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "native-http-get-08" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:49:35 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:native-http-get-08" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.999999999981128, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "node-network-event-9" + } + ] + } +} diff --git a/output/playwright/pr-64/artifacts/native-runtime.json b/output/playwright/pr-64/artifacts/native-runtime.json new file mode 100644 index 0000000..075aa33 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-runtime.json @@ -0,0 +1,76 @@ +{ + "schemaVersion": 1, + "productCommit": "449d47db89109e826eb0e7e0584777365eac3f9b", + "package": { + "name": "node-network-devtools", + "version": "2.0.0" + }, + "tarballSha256": "e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac", + "node": "v24.16.0", + "platform": "darwin-arm64", + "backend": "native", + "selectedMode": "native", + "fallbackReason": null, + "capabilities": { + "http": true, + "https": true, + "fetch": true, + "http2": false, + "responseBody": true, + "requestBody": false, + "websocketLifecycle": true, + "websocketFrames": false, + "sseMessages": false, + "initiator": true + }, + "target": { + "id": "c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "title": "output/playwright/pr-64/manual-evidence-server.mjs", + "type": "node", + "url": "file:///manual-evidence-server.mjs", + "webSocketDebuggerUrl": "ws://127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "discoveryUrl": "http://127.0.0.1:58927/json/list" + }, + "discovery": [ + { + "description": "node.js instance", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico", + "id": "c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "title": "output/playwright/pr-64/manual-evidence-server.mjs", + "type": "node", + "url": "file:///manual-evidence-server.mjs", + "webSocketDebuggerUrl": "ws://127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5" + } + ], + "discoveryContract": { + "list": { + "ok": true, + "targetCount": 1, + "targetIdMatches": true + }, + "version": { + "ok": true, + "browser": "node.js/v24.16.0", + "protocolVersion": "1.1" + }, + "protocol": { + "ok": true, + "domainCount": 14, + "networkDomain": true, + "networkCommandCount": 6, + "networkEventCount": 8 + } + }, + "frontendUrl": "http://127.0.0.1:58933/devtools/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5&hl=en-US", + "controlUrl": "http://127.0.0.1:58934", + "originalFunctionsPreserved": { + "fetch": true, + "httpRequest": true, + "httpsRequest": true + }, + "sourceSha256": "46f81eb2930040a31f402cf4cb113758628eb72c9db27617babba1d3df31af27" +} diff --git a/output/playwright/pr-64/artifacts/native-session-manifest.json b/output/playwright/pr-64/artifacts/native-session-manifest.json new file mode 100644 index 0000000..1661ea9 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-session-manifest.json @@ -0,0 +1,354 @@ +{ + "schemaVersion": 1, + "sessionId": "96007c32-4e84-481a-8012-953ccaac8a57", + "state": "completed", + "createdAt": "2026-08-28T07:46:29.081Z", + "target": { + "id": "c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "title": "output/playwright/pr-64/manual-evidence-server.mjs", + "type": "node", + "url": "file:///manual-evidence-server.mjs", + "webSocketDebuggerUrl": "ws://127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:58927/c91298d7-d6f3-4ef5-8a54-f6ef4a0393b5", + "discoveryUrl": "http://127.0.0.1:58927/json/list" + }, + "files": { + "events": "events.ndjson", + "bodies": "bodies" + }, + "completedAt": "2026-08-28T07:49:48.484Z", + "stats": { + "eventCount": 24, + "requestCount": 9, + "bodyCount": 6, + "bodyErrorCount": 0, + "failedRequestCount": 1 + }, + "requestIndex": { + "node-network-event-1": { + "requestId": "node-network-event-1", + "firstSequence": 1, + "requestTimestamp": 86.3044, + "wallTime": 1787903275119, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:58925/get?token=native-http-get-01", + "method": "GET", + "headers": { + "host": "127.0.0.1:58925" + }, + "hasPostData": false + }, + "responseTimestamp": 86.3068, + "response": { + "url": "http://127.0.0.1:58925/get?token=native-http-get-01", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/plain; charset=utf-8", + "content-length": "38", + "x-manual-case": "native-http-get-01", + "date": "Fri, 28 Aug 2026 07:47:55 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "mimeType": "text/plain", + "charset": "utf-8" + }, + "finishedTimestamp": 86.3071 + }, + "node-network-event-2": { + "requestId": "node-network-event-2", + "firstSequence": 4, + "requestTimestamp": 90.2372, + "wallTime": 1787903279052, + "resourceType": "Other", + "request": { + "url": "https://127.0.0.1:58926/secure-get?token=native-https-get-02", + "method": "GET", + "headers": { + "host": "127.0.0.1:58926" + }, + "hasPostData": false + }, + "responseTimestamp": 90.245, + "response": { + "url": "https://127.0.0.1:58926/secure-get?token=native-https-get-02", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/plain; charset=utf-8", + "content-length": "41", + "x-manual-case": "native-https-get-02", + "date": "Fri, 28 Aug 2026 07:47:59 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "mimeType": "text/plain", + "charset": "utf-8" + }, + "finishedTimestamp": 90.2455 + }, + "node-network-event-3": { + "requestId": "node-network-event-3", + "firstSequence": 7, + "requestTimestamp": 94.2859, + "wallTime": 1787903283101, + "resourceType": "Fetch", + "request": { + "url": "http://127.0.0.1:58925/post?token=native-fetch-post-03", + "method": "POST", + "headers": { + "content-type": "text/plain; charset=utf-8", + "x-manual-case": "native-fetch-post-03", + "accept": "*/*", + "accept-language": "*", + "sec-fetch-mode": "cors", + "user-agent": "node", + "accept-encoding": "gzip, deflate" + }, + "hasPostData": true + }, + "responseTimestamp": 94.2956, + "response": { + "url": "http://127.0.0.1:58925/post?token=native-fetch-post-03", + "status": 201, + "statusText": "Created", + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "89", + "x-manual-case": "native-fetch-post-03", + "Date": "Fri, 28 Aug 2026 07:48:03 GMT", + "Connection": "keep-alive", + "Keep-Alive": "timeout=5" + }, + "mimeType": "application/json", + "charset": "utf-8" + }, + "finishedTimestamp": 94.296 + }, + "node-network-event-4": { + "requestId": "node-network-event-4", + "firstSequence": 10, + "requestTimestamp": 98.2512, + "wallTime": 1787903287066, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:58925/trace?token=native-trace-04", + "method": "GET", + "headers": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64", + "host": "127.0.0.1:58925" + }, + "hasPostData": false + }, + "trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + }, + "responseTimestamp": 98.2539, + "response": { + "url": "http://127.0.0.1:58925/trace?token=native-trace-04", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "application/json; charset=utf-8", + "content-length": "133", + "date": "Fri, 28 Aug 2026 07:48:07 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "mimeType": "application/json", + "charset": "utf-8" + }, + "finishedTimestamp": 98.2542 + }, + "node-network-event-5": { + "requestId": "node-network-event-5", + "firstSequence": 13, + "requestTimestamp": 102.21, + "wallTime": 1787903291025, + "request": { + "url": "http://127.0.0.1:58925/reset?token=native-failed-05", + "method": "GET", + "headers": { + "host": "127.0.0.1:58925" + }, + "hasPostData": false + }, + "finishedTimestamp": 102.212, + "failure": { + "errorText": "socket hang up" + } + }, + "node-network-event-6": { + "requestId": "node-network-event-6", + "firstSequence": 15, + "requestTimestamp": 106.193, + "wallTime": 1787903295008, + "resourceType": "Fetch", + "request": { + "url": "http://127.0.0.1:58925/sse?token=native-sse-06", + "method": "GET", + "headers": { + "accept": "*/*", + "accept-language": "*", + "sec-fetch-mode": "cors", + "user-agent": "node", + "accept-encoding": "gzip, deflate" + }, + "hasPostData": false + }, + "responseTimestamp": 106.198, + "response": { + "url": "http://127.0.0.1:58925/sse?token=native-sse-06", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/event-stream; charset=utf-8", + "content-length": "81", + "cache-control": "no-cache", + "Date": "Fri, 28 Aug 2026 07:48:15 GMT", + "Connection": "keep-alive", + "Keep-Alive": "timeout=5" + }, + "mimeType": "text/event-stream", + "charset": "utf-8" + }, + "finishedTimestamp": 106.198 + }, + "node-network-event-7": { + "requestId": "node-network-event-7", + "firstSequence": 18, + "requestTimestamp": 110.152, + "wallTime": 1787903298967, + "request": { + "url": "http://127.0.0.1:58925/websocket?token=native-websocket-07", + "method": "GET", + "headers": { + "sec-websocket-key": "iVtSkmOz5zD/H90P5l+npg==", + "sec-websocket-version": "13", + "sec-websocket-extensions": "permessage-deflate; client_max_window_bits", + "accept": "*/*", + "accept-language": "*", + "sec-fetch-mode": "websocket", + "user-agent": "node", + "pragma": "no-cache", + "cache-control": "no-cache", + "accept-encoding": "gzip, deflate" + }, + "hasPostData": false + } + }, + "node-network-event-8": { + "requestId": "node-network-event-8", + "firstSequence": 19 + }, + "node-network-event-9": { + "requestId": "node-network-event-9", + "firstSequence": 22, + "requestTimestamp": 186.86, + "wallTime": 1787903375674, + "resourceType": "Other", + "request": { + "url": "http://127.0.0.1:58925/get?token=native-http-get-08", + "method": "GET", + "headers": { + "host": "127.0.0.1:58925" + }, + "hasPostData": false + }, + "responseTimestamp": 186.862, + "response": { + "url": "http://127.0.0.1:58925/get?token=native-http-get-08", + "status": 200, + "statusText": "OK", + "headers": { + "content-type": "text/plain; charset=utf-8", + "content-length": "38", + "x-manual-case": "native-http-get-08", + "date": "Fri, 28 Aug 2026 07:49:35 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "mimeType": "text/plain", + "charset": "utf-8" + }, + "finishedTimestamp": 186.863 + } + }, + "bodyIndex": { + "node-network-event-1": { + "requestId": "node-network-event-1", + "path": "bodies/31cc118298853420-8c7e40e0ebf9259cbe9ca6193c5963e56e6cfe3cb357c8771e779d74ee374208.body", + "sha256": "8c7e40e0ebf9259cbe9ca6193c5963e56e6cfe3cb357c8771e779d74ee374208", + "byteLength": 38, + "base64Encoded": false, + "mimeType": "text/plain" + }, + "node-network-event-2": { + "requestId": "node-network-event-2", + "path": "bodies/e83ceaae23523325-1f1c69670cc5d899a65e5b968b2275f0d41e77d46ea3b50a62a3fa611201d6b4.body", + "sha256": "1f1c69670cc5d899a65e5b968b2275f0d41e77d46ea3b50a62a3fa611201d6b4", + "byteLength": 41, + "base64Encoded": false, + "mimeType": "text/plain" + }, + "node-network-event-3": { + "requestId": "node-network-event-3", + "path": "bodies/6ed405c68abbc3e2-f607b5ba8e0e9dd7ac298b7e45424e59d2cdcb21d620d9dc3aabac6ec089a198.body", + "sha256": "f607b5ba8e0e9dd7ac298b7e45424e59d2cdcb21d620d9dc3aabac6ec089a198", + "byteLength": 89, + "base64Encoded": false, + "mimeType": "application/json" + }, + "node-network-event-4": { + "requestId": "node-network-event-4", + "path": "bodies/98fa223a6be7aac1-28b7171e7edc8622367ffe1a9d56d463e5c79846041266e3d122b03a7121622a.body", + "sha256": "28b7171e7edc8622367ffe1a9d56d463e5c79846041266e3d122b03a7121622a", + "byteLength": 133, + "base64Encoded": false, + "mimeType": "application/json" + }, + "node-network-event-6": { + "requestId": "node-network-event-6", + "path": "bodies/560ae859abcea7a8-b85d7e9db80dcd113d5b1b870dbfe8480f4b087a6c51343f64fb107fda7eecfb.body", + "sha256": "b85d7e9db80dcd113d5b1b870dbfe8480f4b087a6c51343f64fb107fda7eecfb", + "byteLength": 81, + "base64Encoded": false, + "mimeType": "text/event-stream" + }, + "node-network-event-9": { + "requestId": "node-network-event-9", + "path": "bodies/c3a84d1b8ac9c35a-cf096cabf3ea54d3263c5638aee98e7a20fb9394de4b8732d0b4170c0859ba5e.body", + "sha256": "cf096cabf3ea54d3263c5638aee98e7a20fb9394de4b8732d0b4170c0859ba5e", + "byteLength": 38, + "base64Encoded": false, + "mimeType": "text/plain" + } + }, + "traceIndex": { + "4bf92f3577b34da6a3ce929d0e0e4736": { + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "requestIds": ["node-network-event-4"], + "spans": [ + { + "requestId": "node-network-event-4", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=manual-pr64" + } + ] + } + }, + "issues": [] +} diff --git a/output/playwright/pr-64/artifacts/native-session.har b/output/playwright/pr-64/artifacts/native-session.har new file mode 100644 index 0000000..ce30c22 --- /dev/null +++ b/output/playwright/pr-64/artifacts/native-session.har @@ -0,0 +1,691 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "node-network-devtools", + "version": "2" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2026-08-28T07:47:55.119Z", + "time": 2.7000000000043656, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/get?token=native-http-get-01", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-http-get-01" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "native-http-get-01" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:47:55 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:native-http-get-01" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.3999999999944066, + "receive": 0.30000000000995897, + "ssl": -1 + }, + "_requestId": "node-network-event-1" + }, + { + "startedDateTime": "2026-08-28T07:47:59.052Z", + "time": 8.300000000005525, + "request": { + "method": "GET", + "url": "https://127.0.0.1:58926/secure-get?token=native-https-get-02", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58926" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-https-get-02" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "41" + }, + { + "name": "x-manual-case", + "value": "native-https-get-02" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:47:59 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 41, + "mimeType": "text/plain", + "text": "manual-https-response:native-https-get-02" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 41 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 7.800000000003138, + "receive": 0.5000000000023874, + "ssl": -1 + }, + "_requestId": "node-network-event-2" + }, + { + "startedDateTime": "2026-08-28T07:48:03.101Z", + "time": 10.100000000008436, + "request": { + "method": "POST", + "url": "http://127.0.0.1:58925/post?token=native-fetch-post-03", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "x-manual-case", + "value": "native-fetch-post-03" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-language", + "value": "*" + }, + { + "name": "sec-fetch-mode", + "value": "cors" + }, + { + "name": "user-agent", + "value": "node" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-fetch-post-03" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 201, + "statusText": "Created", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "89" + }, + { + "name": "x-manual-case", + "value": "native-fetch-post-03" + }, + { + "name": "Date", + "value": "Fri, 28 Aug 2026 07:48:03 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Keep-Alive", + "value": "timeout=5" + } + ], + "content": { + "size": 89, + "mimeType": "application/json", + "text": "{\"token\":\"native-fetch-post-03\",\"requestBody\":\"manual-request-body:native-fetch-post-03\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 89 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 9.699999999995157, + "receive": 0.4000000000132786, + "ssl": -1 + }, + "_requestId": "node-network-event-3" + }, + { + "startedDateTime": "2026-08-28T07:48:07.066Z", + "time": 3.0000000000001137, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/trace?token=native-trace-04", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "traceparent", + "value": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + }, + { + "name": "tracestate", + "value": "vendor=manual-pr64" + }, + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-trace-04" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "content-length", + "value": "133" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:48:07 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 133, + "mimeType": "application/json", + "text": "{\"token\":\"native-trace-04\",\"traceparent\":\"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\",\"tracestate\":\"vendor=manual-pr64\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 133 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 2.7000000000043656, + "receive": 0.2999999999957481, + "ssl": -1 + }, + "_requestId": "node-network-event-4", + "_trace": { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "version": "00", + "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", + "parentId": "00f067aa0ba902b7", + "traceFlags": "01", + "sampled": true, + "tracestate": "vendor=manual-pr64" + } + }, + { + "startedDateTime": "2026-08-28T07:48:11.025Z", + "time": 2.0000000000095497, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/reset?token=native-failed-05", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-failed-05" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "socket hang up", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "content": { + "size": 0, + "mimeType": "application/octet-stream" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "node-network-event-5", + "_failure": { + "errorText": "socket hang up" + } + }, + { + "startedDateTime": "2026-08-28T07:48:15.008Z", + "time": 4.9999999999954525, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/sse?token=native-sse-06", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-language", + "value": "*" + }, + { + "name": "sec-fetch-mode", + "value": "cors" + }, + { + "name": "user-agent", + "value": "node" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-sse-06" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "content-length", + "value": "81" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "Date", + "value": "Fri, 28 Aug 2026 07:48:15 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Keep-Alive", + "value": "timeout=5" + } + ], + "content": { + "size": 81, + "mimeType": "text/event-stream", + "text": "id: 1\nevent: manual\ndata: sse-native-sse-06\n\nid: 2\ndata: complete-native-sse-06\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 81 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 4.9999999999954525, + "receive": 0, + "ssl": -1 + }, + "_requestId": "node-network-event-6" + }, + { + "startedDateTime": "2026-08-28T07:48:18.967Z", + "time": 0, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/websocket?token=native-websocket-07", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "sec-websocket-key", + "value": "iVtSkmOz5zD/H90P5l+npg==" + }, + { + "name": "sec-websocket-version", + "value": "13" + }, + { + "name": "sec-websocket-extensions", + "value": "permessage-deflate; client_max_window_bits" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-language", + "value": "*" + }, + { + "name": "sec-fetch-mode", + "value": "websocket" + }, + { + "name": "user-agent", + "value": "node" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-websocket-07" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "content": { + "size": 0, + "mimeType": "application/octet-stream" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "node-network-event-7" + }, + { + "startedDateTime": "2026-08-28T07:46:29.081Z", + "time": 0, + "request": { + "method": "GET", + "url": "", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "content": { + "size": 0, + "mimeType": "application/octet-stream" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 0, + "receive": 0, + "ssl": -1 + }, + "_requestId": "node-network-event-8" + }, + { + "startedDateTime": "2026-08-28T07:49:35.674Z", + "time": 2.999999999985903, + "request": { + "method": "GET", + "url": "http://127.0.0.1:58925/get?token=native-http-get-08", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "host", + "value": "127.0.0.1:58925" + } + ], + "queryString": [ + { + "name": "token", + "value": "native-http-get-08" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "content-length", + "value": "38" + }, + { + "name": "x-manual-case", + "value": "native-http-get-08" + }, + { + "name": "date", + "value": "Fri, 28 Aug 2026 07:49:35 GMT" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "keep-alive", + "value": "timeout=5" + } + ], + "content": { + "size": 38, + "mimeType": "text/plain", + "text": "manual-get-response:native-http-get-08" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 38 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": -1, + "wait": 1.999999999981128, + "receive": 1.0000000000047748, + "ssl": -1 + }, + "_requestId": "node-network-event-9" + } + ] + } +} diff --git a/output/playwright/pr-64/checksums.sha256 b/output/playwright/pr-64/checksums.sha256 new file mode 100644 index 0000000..c4dba30 --- /dev/null +++ b/output/playwright/pr-64/checksums.sha256 @@ -0,0 +1,51 @@ +f34efafe391d151dde34d036eab4e101d41a79fef639d89667b4a2c7d7edc34b artifacts/cli-manual-results.json +d4eca8f827e76e30fd41456143c8f8e821b858b6258b9ae5b7f3f823eff884ca artifacts/cli-replay-fixture.har +ef734c395e25dec7edf8c58f08bdcf200e6504890d2639242c7faff0c5d4f361 artifacts/legacy-dispose-summary.json +1c8dc82c4389c8398ffcc858a9887b4eec3c92252d96214bb0fd1e1b95c41f6e artifacts/legacy-events.ndjson +c4199e87cde4792f0d5659a8dba6f6bc87d9d38fd24e5e17fe5b4c07e9b5d2be artifacts/legacy-finalize-summary.json +d0cfb108f63d85fdb71a038a0b5d55b3752cbb194492063df118312ca8267961 artifacts/legacy-replayable.har +4ee556d247e811da5377a2b313284b7b787509ed79aa7fa8959362aec1de44e2 artifacts/legacy-runtime.json +6fd54e0ef211120b3a38ea623b349809194ae4b76937e2ff2cd10f0d851c87dd artifacts/legacy-session-manifest.json +71ba517f3b9922b36f53e9eac7dda3d96cc3fc787dfd80142f9f34df58872587 artifacts/legacy-session.har +ef734c395e25dec7edf8c58f08bdcf200e6504890d2639242c7faff0c5d4f361 artifacts/native-dispose-summary.json +7f19518e3f71418f7d94ad8085ac6c538a9b721b3e31dc285709390ed8069df3 artifacts/native-events.ndjson +865d93beae666def9e9fa07eea2dc89ee557e68cdd03410cc71acb857c71612a artifacts/native-finalize-summary.json +8089726ff924c3dc62c66b4cd05bb206a4328b4e36c4cc81288740b2ebac2390 artifacts/native-http2-probe.json +a9db6dc74b322c39307d8ca0276672377873ddcf0d9c39b3ec3d2349cf6f4b84 artifacts/native-replayable.har +50023a8d9644d373a32750e09b7c601a1f1601aa1aba40528d79ff5b561f7d50 artifacts/native-runtime.json +38612d7fc0de6a610765f14663e6dd0dc04eb86713ed3ed69e634984fbefccc0 artifacts/native-session-manifest.json +eeaee3411588315d22bea3b5bc6d49801c42e562022ffda382fe63f8bd0be1e7 artifacts/native-session.har +0c322878c3ac124fe8cfeff269ef8ed87a15fbfc81a59639611f9a036a260390 manifest.json +fa7a9fee5c940e725c37414bce03dba5a349e43a13d1a230750657e22626a083 manual-cli-case.sh +b0f2501a432a19513d19883b7d2efd83dbdaa451a3a9eda1fcfe06131469ec1e manual-cli-evidence-server.mjs +46f81eb2930040a31f402cf4cb113758628eb72c9db27617babba1d3df31af27 manual-evidence-server.mjs +7975bd338de114fc682a4942688496fa0d53945ef400a7b4069f748b0ad0d18b manual-localhost-cert.pem +454d203a290b11eda450af412047157a1cdf142cadf4c7a6c2c59b59776f38fb manual-localhost-key.pem +5a16264481f0c09e1db3200432be162acddc7573b5053b35f2ff0eb327878cfc manual-native-http2-probe.mjs +99e0983a5ee31178beecddf0b8b10f19ba7f04b36ed8b4fbe8b7b93527d1fdd6 manual-native-mock-conflict.mjs +0d1df924aaef8eac60d7c60b77d55e3856c51c9e32d1878888afcf45a2a2df16 screenshots/MT-01-cli-version-doctor-native.png +45c90d44ead071c0821f41e260c634ad48bd50e161a25d226b550936f8008b20 screenshots/MT-02-cli-zero-code-and-conflict.png +c2031cce1cfff68bfa763aef8839c7729d1b54a2cd8f31a8fdebcb2db4cfbe71 screenshots/MT-03-native-runtime-and-capabilities.png +8f31650d6374c3ab49fed9fa49301b99bb57a41e5b3b0758953b4a12081466f5 screenshots/MT-04-native-version.png +072e937ab5a7de08f150fd5fdc98294cc71fc87ac60da59596c309655df619e1 screenshots/MT-05-native-devtools-network-list.png +dc3ec6a367ca6143772eb4bc4b71db44b11524cd737b4e0ab1e4f8f9142bc391 screenshots/MT-05-native-fetch-headers.png +da6d4cbd7f5158605788007086ddb49c259cf1e60a06c2eb8495c97e60053616 screenshots/MT-05-native-fetch-response.png +2c6024dd1510383b7fd6c22ebef6f45e9cebac39911b0543f1ef1ebf73d07ce9 screenshots/MT-06-native-devtools-reload-reconnect.png +16b537e7df8e9ec90ec175c55e8d196dc10dea2d96f6cbe29c153a8561557be6 screenshots/MT-07-legacy-runtime-and-capabilities.png +51f37216e97186cda052b151dc95941d8644a9316a676df7362598e61bdb637f screenshots/MT-08-legacy-version.png +1908657aa0ee8a8e52173c247994a28e8334a464b93b4296e569ab7384d28d34 screenshots/MT-09-legacy-devtools-network-list.png +dc9e4be668ef3af1ffef8ec3885ac65497d0fdf7776572dc50c81e43937886d2 screenshots/MT-09-legacy-fetch-payload.png +6c3c5e61cc01c52c0c360c7c0a5a8c96af35d19af56bf0b815b2dc15933c39e0 screenshots/MT-09-legacy-fetch-response.png +fcaa05f2189a06149dfeb9c33fa8e7e15f2a58ff5bc6619e1a7fc7d0c6cf429a screenshots/MT-10-legacy-mock-fetch-response.png +79b0877b95f98d1f33e9c909d05127d3e0281ae2bce9ca5f01b5e02dde0daf74 screenshots/MT-10-legacy-mock-http-response.png +ee7ba9a6291e7cb424dbbd987625f2359c6a74c7ef65810f8c9ce3fbe5a12aa5 screenshots/MT-11-legacy-sse-eventstream.png +502304fb3ebb7fe3cb5de7b1246a7fc495963a6b89223f4f275dd8816bfd320d screenshots/MT-11-legacy-websocket-messages.png +ff34e249b69ead76153c195a8508faa120db07aebbfe54cf533442d838012720 screenshots/MT-11-native-websocket-lifecycle.png +3eb522a9f6a3e0d677899d88a6bb5823ebdc899c9c5d687ce1c37706d3e55df6 screenshots/MT-12-legacy-session-har-replay-trace.png +95be16feaaab913174c623d0bf79c0730f16a0117254add4495b3c714d06d8c4 screenshots/MT-12-native-session-har-replay-trace.png +0e5297005a8e58f18e4626641ace09f24d08d37c84ad69886af21dbc06235011 screenshots/MT-13-legacy-boundary-assertions.png +fd2338052112843746471657b006d8392b73f9f9e04850be03a7a73aa663ec09 screenshots/MT-13-legacy-failed-request.png +a9c3c8df80a4447b35edcd66102c692624675378f860e2bceceeeb669309f99f screenshots/MT-13-native-boundary-assertions.png +f0ce25b01a86e27d509ba14b0ca3ed5b9256ede0f4bc776e7a21ddcc98901885 screenshots/MT-13-native-failed-request.png +fb8aba5f739fc8ae4803ffaf516606c7e313124a62fcad96c01c876cfd1943a7 screenshots/MT-14-legacy-dispose-cleanup.png +ae56050e91183a777974c7d9e14493239d51e5ace2146fd43ae1be440066b5c0 screenshots/MT-14-native-dispose-cleanup.png diff --git a/output/playwright/pr-64/manifest.json b/output/playwright/pr-64/manifest.json new file mode 100644 index 0000000..3e38816 --- /dev/null +++ b/output/playwright/pr-64/manifest.json @@ -0,0 +1,193 @@ +{ + "schemaVersion": 1, + "testRunId": "pr-64-manual-acceptance-2026-08-28", + "pullRequest": "https://github.com/GrinZero/node-network-devtools/pull/64", + "testedProductCommit": "449d47db89109e826eb0e7e0584777365eac3f9b", + "package": { + "name": "node-network-devtools", + "version": "2.0.0", + "packedFileCount": 190, + "tarballSha256": "e97dc360d2fcd5d141b13bca490f603b802dc7fe94f3b6a06a690c7a7f48e2ac" + }, + "environment": { + "node": "v24.16.0", + "compatibilityProbeNodes": ["v22.22.3", "v24.16.0", "v26.8.1"], + "platform": "darwin-arm64", + "browser": "Chrome for Testing 151.0.7922.34", + "playwrightCli": "0.1.18", + "playwrightEngine": "1.63.0-alpha-2026-08-05", + "timezone": "Asia/Shanghai", + "date": "2026-08-28" + }, + "harness": { + "manualEvidenceServerSha256": "46f81eb2930040a31f402cf4cb113758628eb72c9db27617babba1d3df31af27" + }, + "result": { + "status": "PASS", + "passed": 14, + "failed": 0, + "total": 14, + "screenshots": 26, + "structuredArtifacts": 17, + "reproductionHarnesses": 5, + "testTlsInputs": 2 + }, + "cases": [ + { + "id": "MT-01", + "title": "Exact package version and Native doctor", + "status": "PASS", + "evidence": [ + "screenshots/MT-01-cli-version-doctor-native.png", + "artifacts/cli-manual-results.json" + ] + }, + { + "id": "MT-02", + "title": "Zero-code CLI modes, conflict, replay, and exact-target open", + "status": "PASS", + "evidence": [ + "screenshots/MT-02-cli-zero-code-and-conflict.png", + "artifacts/cli-manual-results.json" + ] + }, + { + "id": "MT-03", + "title": "Native registration and capability disclosure", + "status": "PASS", + "evidence": [ + "screenshots/MT-03-native-runtime-and-capabilities.png", + "artifacts/native-runtime.json" + ] + }, + { + "id": "MT-04", + "title": "Native standard CDP discovery contract", + "status": "PASS", + "evidence": [ + "screenshots/MT-03-native-runtime-and-capabilities.png", + "screenshots/MT-04-native-version.png", + "artifacts/native-runtime.json" + ] + }, + { + "id": "MT-05", + "title": "Native HTTP, HTTPS, and Fetch in official DevTools", + "status": "PASS", + "evidence": [ + "screenshots/MT-05-native-devtools-network-list.png", + "screenshots/MT-05-native-fetch-headers.png", + "screenshots/MT-05-native-fetch-response.png", + "artifacts/native-events.ndjson" + ] + }, + { + "id": "MT-06", + "title": "Native DevTools reload and reconnect", + "status": "PASS", + "evidence": [ + "screenshots/MT-06-native-devtools-reload-reconnect.png", + "artifacts/native-session-manifest.json" + ] + }, + { + "id": "MT-07", + "title": "Auto-to-Legacy selection and capability disclosure", + "status": "PASS", + "evidence": [ + "screenshots/MT-07-legacy-runtime-and-capabilities.png", + "artifacts/legacy-runtime.json" + ] + }, + { + "id": "MT-08", + "title": "Legacy standard CDP discovery contract", + "status": "PASS", + "evidence": [ + "screenshots/MT-07-legacy-runtime-and-capabilities.png", + "screenshots/MT-08-legacy-version.png", + "artifacts/legacy-runtime.json" + ] + }, + { + "id": "MT-09", + "title": "Legacy HTTP, HTTPS, and Fetch details in official DevTools", + "status": "PASS", + "evidence": [ + "screenshots/MT-09-legacy-devtools-network-list.png", + "screenshots/MT-09-legacy-fetch-payload.png", + "screenshots/MT-09-legacy-fetch-response.png", + "artifacts/legacy-events.ndjson" + ] + }, + { + "id": "MT-10", + "title": "Legacy HTTP and Fetch mocking with zero origin leakage", + "status": "PASS", + "evidence": [ + "screenshots/MT-10-legacy-mock-http-response.png", + "screenshots/MT-10-legacy-mock-fetch-response.png", + "artifacts/legacy-finalize-summary.json" + ] + }, + { + "id": "MT-11", + "title": "Native and Legacy SSE/WebSocket capability boundaries", + "status": "PASS", + "evidence": [ + "screenshots/MT-11-native-websocket-lifecycle.png", + "screenshots/MT-11-legacy-sse-eventstream.png", + "screenshots/MT-11-legacy-websocket-messages.png", + "artifacts/native-finalize-summary.json", + "artifacts/legacy-finalize-summary.json" + ] + }, + { + "id": "MT-12", + "title": "Session, HAR, Replay, and Trace on both backends", + "status": "PASS", + "evidence": [ + "screenshots/MT-12-native-session-har-replay-trace.png", + "screenshots/MT-12-legacy-session-har-replay-trace.png", + "artifacts/cli-manual-results.json", + "artifacts/native-finalize-summary.json", + "artifacts/legacy-finalize-summary.json", + "artifacts/native-session.har", + "artifacts/legacy-session.har" + ] + }, + { + "id": "MT-13", + "title": "Failed lifecycle and Native HTTP/2 capability boundary", + "status": "PASS", + "evidence": [ + "screenshots/MT-13-native-failed-request.png", + "screenshots/MT-13-legacy-failed-request.png", + "screenshots/MT-13-native-boundary-assertions.png", + "screenshots/MT-13-legacy-boundary-assertions.png", + "artifacts/native-http2-probe.json", + "artifacts/native-finalize-summary.json", + "artifacts/legacy-finalize-summary.json" + ] + }, + { + "id": "MT-14", + "title": "Dispose and closed-endpoint verification", + "status": "PASS", + "evidence": [ + "screenshots/MT-14-native-dispose-cleanup.png", + "screenshots/MT-14-legacy-dispose-cleanup.png", + "artifacts/native-dispose-summary.json", + "artifacts/legacy-dispose-summary.json" + ] + } + ], + "notes": [ + "All business traffic was loopback-only.", + "Native requestBody, websocketFrames, and sseMessages remain false on the primary Node 24 run and were not represented as supported.", + "Native HTTP/2 is advertised only on the verified Node 22.20+ 22.x line; the expected Node 24/26 upstream crash reproductions prove why the capability is withheld there.", + "The CLI --open OS launch was redirected to an exact-target CDP verifier; separate screenshots prove a real visible connection to the official Chromium DevTools frontend.", + "Local filesystem prefixes were redacted from retained text, and raw discovery screenshots containing local file URLs were excluded.", + "Screenshots satisfy the requested visual-evidence option; no video is committed." + ] +} diff --git a/output/playwright/pr-64/manual-cli-case.sh b/output/playwright/pr-64/manual-cli-case.sh new file mode 100644 index 0000000..1582ab5 --- /dev/null +++ b/output/playwright/pr-64/manual-cli-case.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +backend="${1:-}" +if [[ "$backend" != "native" && "$backend" != "legacy" ]]; then + echo "usage: bash manual-cli-case.sh " >&2 + exit 2 +fi + +evidence_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$evidence_root/../../.." && pwd)" +nnd="$evidence_root/consumer/node_modules/.bin/nnd" +fixture="$repo_root/packages/network-debugger/test/e2e/cli/fixtures/probe.mjs" + +"$nnd" dev --no-wait --mode "$backend" "$fixture" 2>/dev/null \ + | sed -n 's/^@@NND_E2E@@//p' \ + | jq '{type,label,preloadInjected,mode,execArgv,capabilities}' diff --git a/output/playwright/pr-64/manual-cli-evidence-server.mjs b/output/playwright/pr-64/manual-cli-evidence-server.mjs new file mode 100644 index 0000000..f47ca6f --- /dev/null +++ b/output/playwright/pr-64/manual-cli-evidence-server.mjs @@ -0,0 +1,456 @@ +import { spawn } from 'node:child_process' +import { createServer } from 'node:http' +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const evidenceRoot = dirname(fileURLToPath(import.meta.url)) +const repoRoot = resolve(evidenceRoot, '../../..') +const nndPath = resolve(evidenceRoot, 'consumer/node_modules/.bin/nnd') +const fixturePath = resolve(repoRoot, 'packages/network-debugger/test/e2e/cli/fixtures/probe.mjs') +const conflictPath = resolve(evidenceRoot, 'manual-native-mock-conflict.mjs') +const artifactPath = resolve(evidenceRoot, 'artifacts/cli-manual-results.json') +const replayFixturePath = resolve(evidenceRoot, 'artifacts/cli-replay-fixture.har') +const openRecordPath = resolve(evidenceRoot, '.runtime/cli-open.ndjson') +const frontendOpenHookPath = resolve( + repoRoot, + 'packages/network-debugger/test/e2e/cli/fixtures/frontend-open-hook.cjs' +) +const frontendRunnerPath = resolve( + repoRoot, + 'packages/network-debugger/test/e2e/cli/fixtures/frontend-cdp-runner.mjs' +) +const productCommit = process.env.NND_MANUAL_PRODUCT_COMMIT ?? 'unknown' +const tarballSha256 = process.env.NND_MANUAL_TARBALL_SHA256 ?? 'unknown' + +const results = new Map() +const replayOriginRecords = [] +let baseUrl + +const pathRedactions = [ + [resolve(evidenceRoot, 'consumer'), ''], + [evidenceRoot, ''], + [repoRoot, ''] +] + +function redactEvidence(value) { + if (typeof value === 'string') { + return pathRedactions.reduce( + (redacted, [path, replacement]) => redacted.replaceAll(path, replacement), + value + ) + } + if (Array.isArray(value)) return value.map(redactEvidence) + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([name, entry]) => [name, redactEvidence(entry)]) + ) + } + return value +} + +function run(command, args, { env = {}, timeoutMs = 20_000 } = {}) { + return new Promise((resolveRun, rejectRun) => { + const child = spawn(command, args, { + cwd: repoRoot, + env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', ...env }, + stdio: ['ignore', 'pipe', 'pipe'] + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (stdout += chunk)) + child.stderr.on('data', (chunk) => (stderr += chunk)) + const timeout = setTimeout(() => { + child.kill('SIGTERM') + rejectRun(new Error(`Command timed out after ${timeoutMs}ms: ${command} ${args.join(' ')}`)) + }, timeoutMs) + child.once('error', (error) => { + clearTimeout(timeout) + rejectRun(error) + }) + child.once('close', (exitCode, signal) => { + clearTimeout(timeout) + resolveRun({ exitCode, signal, stdout: stdout.trim(), stderr: stderr.trim() }) + }) + }) +} + +function assert(condition, message) { + if (!condition) throw new Error(message) +} + +function parseFixtureMarker(stdout, label) { + const marker = stdout.split('\n').find((line) => line.startsWith('@@NND_E2E@@')) + assert(marker, `${label} fixture marker missing`) + return JSON.parse(marker.slice('@@NND_E2E@@'.length)) +} + +async function waitForFile(path, timeoutMs = 5_000) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + return await readFile(path, 'utf8') + } catch (error) { + if (error?.code !== 'ENOENT') throw error + await new Promise((resolvePromise) => setTimeout(resolvePromise, 25)) + } + } + throw new Error(`Timed out waiting for ${path}`) +} + +async function writeReplayFixture() { + const har = { + log: { + version: '1.2', + creator: { name: 'PR #64 manual evidence', version: '1' }, + pages: [], + entries: [ + { + request: { + method: 'GET', + url: `${baseUrl}/replay-get?case=cli-public-replay`, + httpVersion: 'HTTP/1.1', + headers: [], + queryString: [{ name: 'case', value: 'cli-public-replay' }], + cookies: [], + headersSize: -1, + bodySize: 0 + }, + response: {}, + cache: {}, + timings: {}, + _requestId: 'cli-replay-get' + }, + { + request: { + method: 'POST', + url: `${baseUrl}/replay-post?case=cli-public-replay`, + httpVersion: 'HTTP/1.1', + headers: [{ name: 'content-type', value: 'text/plain; charset=utf-8' }], + queryString: [{ name: 'case', value: 'cli-public-replay' }], + cookies: [], + headersSize: -1, + bodySize: 22, + postData: { + mimeType: 'text/plain; charset=utf-8', + text: 'cli-replay-request-body' + } + }, + response: {}, + cache: {}, + timings: {}, + _requestId: 'cli-replay-post' + } + ] + } + } + await mkdir(dirname(replayFixturePath), { recursive: true }) + await writeFile(replayFixturePath, `${JSON.stringify(har, null, 2)}\n`, 'utf8') +} + +async function runCase(id) { + if (id === 'doctor') { + const versionExecution = await run(process.execPath, [nndPath, '--version']) + assert(versionExecution.exitCode === 0, `version exited ${versionExecution.exitCode}`) + const version = versionExecution.stdout.trim() + assert(version === '2.0.0', `version must be 2.0.0, got ${version}`) + const execution = await run(process.execPath, [ + '--experimental-network-inspection', + nndPath, + 'doctor', + '--json' + ]) + assert(execution.exitCode === 0, `doctor exited ${execution.exitCode}`) + const value = JSON.parse(execution.stdout) + assert(value.ok === true, 'doctor ok must be true') + assert(value.packageVersion === '2.0.0', 'packageVersion must be 2.0.0') + assert(value.experimentalFlag === true, 'experimental flag must be detected') + assert(value.selection?.selected === 'native', 'doctor must select Native') + assert(value.networkMethods?.missingRequired?.length === 0, 'required methods must exist') + return { + id, + title: 'CLI version + Native doctor', + status: 'PASS', + command: `nnd --version && node --experimental-network-inspection ${nndPath} doctor --json`, + actual: { + version, + schemaVersion: value.schemaVersion, + ok: value.ok, + nodeVersion: value.nodeVersion, + packageVersion: value.packageVersion, + experimentalFlag: value.experimentalFlag, + missingRequired: value.networkMethods.missingRequired, + selection: value.selection, + capabilities: value.capabilities + } + } + } + + if (id === 'native' || id === 'legacy') { + const execution = await run(process.execPath, [ + nndPath, + 'dev', + '--no-wait', + '--mode', + id, + fixturePath + ]) + assert(execution.exitCode === 0, `${id} dev exited ${execution.exitCode}`) + const value = parseFixtureMarker(execution.stdout, id) + assert(value.preloadInjected === true, `${id} preload must be injected`) + assert(value.mode === id, `${id} selected mode mismatch`) + assert(value.target?.discoveryUrl, `${id} discovery URL missing`) + return { + id, + title: `Zero-code nnd dev · ${id.toUpperCase()}`, + status: 'PASS', + command: `nnd dev --no-wait --mode ${id} ${fixturePath}`, + actual: { + type: value.type, + preloadInjected: value.preloadInjected, + mode: value.mode, + execArgv: value.execArgv, + target: value.target, + capabilities: value.capabilities + } + } + } + + if (id === 'conflict') { + const execution = await run(process.execPath, [conflictPath]) + assert(execution.exitCode === 0, `conflict check exited ${execution.exitCode}`) + const value = JSON.parse(execution.stdout) + assert(value.code === 'NND_NATIVE_MOCK_CONFLICT', 'explicit conflict code missing') + return { + id, + title: 'Forced Native + Mock conflict', + status: 'PASS', + command: `node ${conflictPath}`, + actual: value + } + } + + if (id === 'replay') { + await writeReplayFixture() + replayOriginRecords.length = 0 + const dryExecution = await run(process.execPath, [ + nndPath, + 'replay', + '--dry-run', + '--json', + replayFixturePath + ]) + assert(dryExecution.exitCode === 0, `replay dry-run exited ${dryExecution.exitCode}`) + const dryRun = JSON.parse(dryExecution.stdout) + assert(dryRun.dryRun === true, 'CLI replay dry-run flag missing') + assert(dryRun.succeeded === 2 && dryRun.failed === 0, 'CLI replay dry-run did not pass 2/2') + assert(replayOriginRecords.length === 0, 'CLI replay dry-run performed network I/O') + + const realExecution = await run(process.execPath, [ + nndPath, + 'replay', + '--json', + '--timeout', + '5000', + replayFixturePath + ]) + assert(realExecution.exitCode === 0, `real replay exited ${realExecution.exitCode}`) + const real = JSON.parse(realExecution.stdout) + assert(real.dryRun === false, 'real CLI replay was unexpectedly dry') + assert(real.succeeded === 2 && real.failed === 0, 'real CLI replay did not pass 2/2') + assert(replayOriginRecords.length === 2, 'real CLI replay did not reach both endpoints') + assert( + replayOriginRecords.some( + (record) => record.method === 'POST' && record.body === 'cli-replay-request-body' + ), + 'real CLI replay did not preserve the POST body' + ) + return { + id, + title: 'Public nnd replay · dry + real', + status: 'PASS', + command: `nnd replay --dry-run --json ${replayFixturePath} && nnd replay --json ${replayFixturePath}`, + actual: { dryRun, real, originRequests: [...replayOriginRecords] } + } + } + + if (id === 'open') { + await mkdir(dirname(openRecordPath), { recursive: true }) + await rm(openRecordPath, { force: true }) + const execution = await run( + process.execPath, + [ + '--require', + frontendOpenHookPath, + nndPath, + 'dev', + '--open', + '--mode', + 'native', + fixturePath + ], + { + env: { + NND_E2E_FRONTEND_RUNNER: frontendRunnerPath, + NND_E2E_FRONTEND_RECORD: openRecordPath + } + } + ) + assert(execution.exitCode === 0, `nnd dev --open exited ${execution.exitCode}`) + const value = parseFixtureMarker(execution.stdout, 'open') + const opened = (await waitForFile(openRecordPath)) + .trim() + .split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)) + assert(value.preloadInjected === true, 'open case preload must be injected') + assert(value.mode === 'native', 'open case must select Native') + assert(opened.length === 1, `authoritative target opened ${opened.length} times`) + assert( + opened[0].webSocketDebuggerUrl === value.target.webSocketDebuggerUrl, + 'opened socket does not match the CLI target' + ) + assert(opened[0].frontendUrl.startsWith('devtools://'), 'opened URL is not DevTools') + return { + id, + title: 'nnd dev --open · authoritative target', + status: 'PASS', + command: `nnd dev --open --mode native ${fixturePath}`, + actual: { + preloadInjected: value.preloadInjected, + mode: value.mode, + inspectWait: value.execArgv.find((argument) => argument.startsWith('--inspect-wait=')), + target: value.target, + openedExactlyOnce: opened.length === 1, + openedFrontend: opened[0], + launcherVerification: 'OS browser spawn redirected to an exact-target CDP verifier' + } + } + } + + throw new Error(`Unknown case: ${id}`) +} + +async function saveResults() { + await mkdir(dirname(artifactPath), { recursive: true }) + await writeFile( + artifactPath, + `${JSON.stringify( + { + schemaVersion: 1, + productCommit, + tarballSha256, + cases: [...results.values()] + }, + null, + 2 + )}\n` + ) +} + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +function render() { + const definitions = [ + ['doctor', 'CLI version + doctor'], + ['native', 'Zero-code Native'], + ['legacy', 'Zero-code Legacy'], + ['conflict', 'Native + Mock conflict'], + ['replay', 'Public replay dry + real'], + ['open', 'CLI --open authoritative target'] + ] + const cards = definitions + .map(([id, title]) => { + const result = results.get(id) + return `
+

${escapeHtml(title)}

${result ? 'PASS' : 'PENDING'}
+ ${ + result + ? `${escapeHtml(result.command)}
${escapeHtml(JSON.stringify(result.actual, null, 2))}
` + : `

Click the case button to run the exact packed package.

` + } +
` + }) + .join('') + + return ` + + PR #64 manual CLI evidence
+
PR #64 · manual acceptance

Exact-package CLI evidence

+

${results.size}/${definitions.length} cases passed · each button launches the installed tarball, not workspace source.

+
Product commit ${escapeHtml(productCommit)}
Package node-network-devtools@2.0.0
Tarball SHA-256 ${escapeHtml(tarballSha256)}
Runner ${escapeHtml(process.version)} · ${escapeHtml(process.platform)}-${escapeHtml(process.arch)}
+
${definitions.map(([id, title]) => ``).join('')}
+
${cards}
+
` +} + +const server = createServer(async (request, response) => { + if (request.url?.startsWith('/replay-get')) { + replayOriginRecords.push({ method: request.method, url: request.url, body: '' }) + response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('cli-replay-get-ok') + return + } + + if (request.url?.startsWith('/replay-post')) { + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + const body = Buffer.concat(chunks).toString('utf8') + replayOriginRecords.push({ method: request.method, url: request.url, body }) + response.writeHead(201, { 'content-type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify({ ok: true, body })) + return + } + + if (request.method === 'POST' && request.url?.startsWith('/run/')) { + const id = request.url.slice('/run/'.length) + try { + results.set(id, redactEvidence(await runCase(id))) + await saveResults() + response.writeHead(204).end() + } catch (error) { + response.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' }) + response.end(error?.stack ?? String(error)) + } + return + } + + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + response.end(render()) +}) + +server.listen(0, '127.0.0.1', () => { + const address = server.address() + baseUrl = `http://127.0.0.1:${address.port}` + console.log(`NND_CLI_MANUAL_READY ${baseUrl}`) +}) + +process.once('SIGINT', () => server.close(() => process.exit(0))) +process.once('SIGTERM', () => server.close(() => process.exit(0))) diff --git a/output/playwright/pr-64/manual-evidence-server.mjs b/output/playwright/pr-64/manual-evidence-server.mjs new file mode 100644 index 0000000..b8e299e --- /dev/null +++ b/output/playwright/pr-64/manual-evidence-server.mjs @@ -0,0 +1,1063 @@ +import { spawn } from 'node:child_process' +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises' +import http from 'node:http' +import https from 'node:https' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' + +const backend = process.argv[2] +if (!['native', 'legacy'].includes(backend)) { + throw new Error('Usage: node manual-evidence-server.mjs ') +} + +const consumerRoot = resolve( + process.env.NND_MANUAL_CONSUMER_ROOT ?? new URL('./consumer', import.meta.url).pathname +) +const evidenceRoot = resolve( + process.env.NND_MANUAL_EVIDENCE_ROOT ?? new URL('.', import.meta.url).pathname +) +const productCommit = process.env.NND_MANUAL_PRODUCT_COMMIT ?? 'unknown' +const tarballSha256 = process.env.NND_MANUAL_TARBALL_SHA256 ?? 'unknown' +const runtimeRoot = join(evidenceRoot, '.runtime') +const artifactsRoot = join(evidenceRoot, 'artifacts') +const sessionDirectory = join(runtimeRoot, `${backend}-session-${Date.now()}`) +const manualRequire = createRequire(join(consumerRoot, 'consumer.cjs')) +const workspaceRequire = createRequire( + new URL('../../../packages/network-debugger/package.json', import.meta.url) +) +const api = manualRequire('node-network-devtools') +const { chromium } = workspaceRequire('@playwright/test') +const WebSocket = manualRequire('ws') +const { WebSocketServer } = WebSocket +const packageDirectory = resolve(dirname(manualRequire.resolve('node-network-devtools')), '..') +const packageManifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8')) +const { register, SessionRecorder, exportHar, replay } = api + +for (const [name, value] of Object.entries({ + register, + SessionRecorder, + exportHar, + replay +})) { + if (typeof value !== 'function') { + throw new Error(`The exact packed package does not export ${name}`) + } +} + +await mkdir(runtimeRoot, { recursive: true }) +await mkdir(artifactsRoot, { recursive: true }) + +function sha256(value) { + return createHash('sha256').update(value).digest('hex') +} + +const pathRedactions = [ + [consumerRoot, ''], + [evidenceRoot, ''] +] + +function redactText(value) { + return pathRedactions.reduce( + (redacted, [path, replacement]) => redacted.replaceAll(path, replacement), + value + ) +} + +function redactEvidence(value) { + if (typeof value === 'string') return redactText(value) + if (Array.isArray(value)) return value.map(redactEvidence) + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([name, entry]) => [name, redactEvidence(entry)]) + ) + } + return value +} + +function jsonResponse(response, status, value) { + const body = `${JSON.stringify(value, null, 2)}\n` + response.writeHead(status, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'cache-control': 'no-store' + }) + response.end(body) +} + +function listen(server) { + return new Promise((resolvePromise, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + const address = server.address() + if (!address || typeof address === 'string') { + reject(new Error('Server did not expose a TCP address')) + return + } + resolvePromise(address.port) + }) + }) +} + +function closeServer(server, sockets) { + if (!server.listening) return Promise.resolve() + return new Promise((resolvePromise) => { + server.close(() => resolvePromise()) + for (const socket of sockets) socket.destroy() + }) +} + +function httpRequest(url, { method = 'GET', headers = {}, body } = {}) { + return new Promise((resolvePromise, reject) => { + const secure = new URL(url).protocol === 'https:' + const request = (secure ? https : http).request( + url, + { method, headers, ...(secure ? { rejectUnauthorized: false } : {}) }, + (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolvePromise({ + status: response.statusCode, + statusText: response.statusMessage, + headers: response.headers, + body: Buffer.concat(chunks).toString('utf8') + }) + }) + } + ) + request.once('error', reject) + request.end(body) + }) +} + +function webSocketRoundTrip(url, token) { + return new Promise((resolvePromise, reject) => { + const socket = new WebSocket(url) + const messages = [] + socket.once('error', reject) + socket.once('open', () => { + socket.send(`client-text:${token}`) + socket.send(Buffer.from([0, 1, 2, 127, 128, 254, 255])) + }) + socket.on('message', (data, isBinary) => { + messages.push({ + isBinary, + value: isBinary ? Buffer.from(data).toString('base64') : Buffer.from(data).toString('utf8') + }) + if (messages.length === 2) socket.close(1000, 'manual-evidence-complete') + }) + socket.once('close', (code, reason) => + resolvePromise({ code, reason: reason.toString(), messages }) + ) + }) +} + +function nativeWebSocketRoundTrip(url, token) { + return new Promise((resolvePromise, reject) => { + const socket = new globalThis.WebSocket(url) + socket.binaryType = 'arraybuffer' + const messages = [] + socket.addEventListener('error', () => reject(new Error('Native WebSocket failed'))) + socket.addEventListener('open', () => { + socket.send(`client-text:${token}`) + socket.send(new Uint8Array([0, 1, 2, 127, 128, 254, 255])) + }) + socket.addEventListener('message', (event) => { + const isBinary = typeof event.data !== 'string' + const value = isBinary ? Buffer.from(event.data).toString('base64') : event.data + messages.push({ isBinary, value }) + if (messages.length === 2) socket.close(1000, 'manual-evidence-complete') + }) + socket.addEventListener('close', (event) => + resolvePromise({ code: event.code, reason: event.reason, messages }) + ) + }) +} + +const originSockets = new Set() +const originRecords = [] +const webSocketServer = new WebSocketServer({ noServer: true }) +const originServer = http.createServer(async (request, response) => { + const url = new URL(request.url, `http://${request.headers.host ?? '127.0.0.1'}`) + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + const requestBody = Buffer.concat(chunks).toString('utf8') + const record = { + sequence: originRecords.length + 1, + timestamp: new Date().toISOString(), + method: request.method, + pathname: url.pathname, + url: url.href, + requestBody, + traceparent: request.headers.traceparent ?? null, + tracestate: request.headers.tracestate ?? null + } + originRecords.push(record) + const token = url.searchParams.get('token') ?? '' + + if (url.pathname === '/get') { + const body = `manual-get-response:${token}` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-manual-case': token + }) + response.end(body) + return + } + + if (url.pathname === '/post') { + const body = JSON.stringify({ token, requestBody }) + response.writeHead(201, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-manual-case': token + }) + response.end(body) + return + } + + if (url.pathname === '/sse') { + const body = `id: 1\nevent: manual\ndata: sse-${token}\n\nid: 2\ndata: complete-${token}\n\n` + response.writeHead(200, { + 'content-type': 'text/event-stream; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'cache-control': 'no-cache' + }) + response.end(body) + return + } + + if (url.pathname === '/trace') { + const body = JSON.stringify({ + token, + traceparent: request.headers.traceparent, + tracestate: request.headers.tracestate + }) + response.writeHead(200, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body) + }) + response.end(body) + return + } + + if (url.pathname === '/reset') { + request.socket.destroy() + return + } + + if (url.pathname.startsWith('/mock-')) { + const body = `ORIGIN_LEAK:${url.pathname}` + response.writeHead(599, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body) + }) + response.end(body) + return + } + + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('not found') +}) +originServer.on('connection', (socket) => { + originSockets.add(socket) + socket.once('close', () => originSockets.delete(socket)) +}) +originServer.on('upgrade', (request, socket, head) => { + const url = new URL(request.url, `http://${request.headers.host ?? '127.0.0.1'}`) + if (url.pathname !== '/websocket') { + socket.destroy() + return + } + originRecords.push({ + sequence: originRecords.length + 1, + timestamp: new Date().toISOString(), + method: request.method, + pathname: url.pathname, + url: url.href, + requestBody: '', + traceparent: null, + tracestate: null + }) + webSocketServer.handleUpgrade(request, socket, head, (webSocket) => { + webSocketServer.emit('connection', webSocket, request) + }) +}) +webSocketServer.on('connection', (socket) => { + socket.on('message', (data, isBinary) => socket.send(data, { binary: isBinary })) +}) +const originPort = await listen(originServer) +const originBaseUrl = `http://127.0.0.1:${originPort}` + +const secureOriginSockets = new Set() +const secureOriginServer = https.createServer( + { + key: await readFile(new URL('./manual-localhost-key.pem', import.meta.url)), + cert: await readFile(new URL('./manual-localhost-cert.pem', import.meta.url)) + }, + async (request, response) => { + const url = new URL(request.url, `https://${request.headers.host ?? '127.0.0.1'}`) + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + const requestBody = Buffer.concat(chunks).toString('utf8') + const token = url.searchParams.get('token') ?? '' + originRecords.push({ + sequence: originRecords.length + 1, + timestamp: new Date().toISOString(), + method: request.method, + pathname: url.pathname, + url: url.href, + requestBody, + traceparent: request.headers.traceparent ?? null, + tracestate: request.headers.tracestate ?? null + }) + if (url.pathname !== '/secure-get') { + response.writeHead(404).end('not found') + return + } + const body = `manual-https-response:${token}` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-manual-case': token + }) + response.end(body) + } +) +secureOriginServer.on('connection', (socket) => { + secureOriginSockets.add(socket) + socket.once('close', () => secureOriginSockets.delete(socket)) +}) +const secureOriginPort = await listen(secureOriginServer) +const secureOriginBaseUrl = `https://127.0.0.1:${secureOriginPort}` + +const mockRules = [ + { + id: 'manual-http-mock', + match: { url: `${originBaseUrl}/mock-http*`, method: 'GET' }, + response: { + status: 207, + statusText: 'Manual Mock HTTP', + headers: { + 'content-type': 'application/json; charset=utf-8', + 'x-nnd-mock': 'http' + }, + body: JSON.stringify({ mocked: true, transport: 'http', source: 'node-network-devtools' }) + } + }, + { + id: 'manual-fetch-mock', + match: { + url: `${originBaseUrl}/mock-fetch*`, + method: 'POST', + headers: { 'x-manual-mock': 'fetch' } + }, + response: { + status: 202, + statusText: 'Manual Mock Fetch', + headers: { + 'content-type': 'application/json; charset=utf-8', + 'x-nnd-mock': 'fetch' + }, + body: JSON.stringify({ mocked: true, transport: 'fetch', source: 'node-network-devtools' }) + } + } +] + +const originalFunctions = { + fetch: globalThis.fetch, + httpRequest: http.request, + httpsRequest: https.request +} +const registration = register({ + mode: backend === 'legacy' ? 'auto' : 'native', + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false }, + legacy: { + serverPort: 0, + ...(backend === 'legacy' ? { mock: mockRules } : {}) + } +}) +const ready = await registration.ready +if (ready.mode !== backend) { + throw new Error(`Expected ${backend}, selected ${ready.mode}`) +} +const discoveryUrl = new URL(ready.target.discoveryUrl) +const [discovery, discoveryVersion, discoveryProtocol] = await Promise.all([ + fetch(discoveryUrl).then((response) => response.json()), + fetch(new URL('/json/version', discoveryUrl)).then((response) => response.json()), + fetch(new URL('/json/protocol', discoveryUrl)).then((response) => response.json()) +]) +if (!Array.isArray(discovery) || !discovery.some((target) => target.id === ready.target.id)) { + throw new Error(`Target ${ready.target.id} is absent from discovery`) +} +const networkDomain = discoveryProtocol.domains?.find( + (domain) => (domain.name ?? domain.domain) === 'Network' +) +if (!networkDomain) throw new Error('/json/protocol does not expose the Network domain') +const discoveryContract = { + list: { + ok: true, + targetCount: discovery.length, + targetIdMatches: discovery.some((target) => target.id === ready.target.id) + }, + version: { + ok: true, + browser: discoveryVersion.Browser ?? discoveryVersion.browser ?? null, + protocolVersion: + discoveryVersion['Protocol-Version'] ?? discoveryVersion.protocolVersion ?? null + }, + protocol: { + ok: true, + domainCount: discoveryProtocol.domains.length, + networkDomain: true, + networkCommandCount: networkDomain.commands?.length ?? 0, + networkEventCount: networkDomain.events?.length ?? 0 + } +} +const recorder = await SessionRecorder.start({ + directory: sessionDirectory, + target: ready.target +}) + +const chromiumUserData = await mkdtemp(join(runtimeRoot, `${backend}-chromium-`)) +const chromiumOutput = { stdout: '', stderr: '' } +const chromiumProcess = spawn( + chromium.executablePath(), + [ + '--headless=new', + '--remote-debugging-port=0', + `--user-data-dir=${chromiumUserData}`, + '--no-first-run', + '--no-default-browser-check', + '--no-sandbox', + '--no-proxy-server', + '--disable-background-networking', + '--disable-component-update', + '--disable-default-apps', + '--disable-domain-reliability', + '--disable-extensions', + '--disable-sync', + '--metrics-recording-only', + '--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1' + ], + { stdio: ['ignore', 'pipe', 'pipe'] } +) +chromiumProcess.stdout.setEncoding('utf8') +chromiumProcess.stderr.setEncoding('utf8') +chromiumProcess.stdout.on('data', (chunk) => (chromiumOutput.stdout += chunk)) +chromiumProcess.stderr.on('data', (chunk) => (chromiumOutput.stderr += chunk)) + +const browserWebSocketUrl = await new Promise((resolvePromise, reject) => { + const timeout = setTimeout( + () => reject(new Error(`Chromium did not expose CDP:\n${chromiumOutput.stderr}`)), + 10_000 + ) + const onData = () => { + const match = chromiumOutput.stderr.match(/DevTools listening on (ws:\/\/[^\s]+)/) + if (!match) return + clearTimeout(timeout) + chromiumProcess.stderr.off('data', onData) + resolvePromise(match[1]) + } + chromiumProcess.stderr.on('data', onData) + chromiumProcess.once('exit', (code, signal) => { + clearTimeout(timeout) + reject(new Error(`Chromium exited before CDP (code=${code}, signal=${signal})`)) + }) +}) +const frontendHost = new URL(browserWebSocketUrl).host +const targetSocket = ready.target.webSocketDebuggerUrl.replace(/^ws:\/\//, '') +const frontendUrl = + `http://${frontendHost}/devtools/js_app.html?experiments=true&v8only=true` + + `&ws=${targetSocket}&hl=en-US` + +let scenarioCounter = 0 +let finalized +let disposed +const scenarioResults = [] + +function scenarioToken(scenario) { + scenarioCounter += 1 + return `${backend}-${scenario}-${String(scenarioCounter).padStart(2, '0')}` +} + +async function runScenario(scenario) { + const token = scenarioToken(scenario) + let result + switch (scenario) { + case 'http-get': + result = await httpRequest(`${originBaseUrl}/get?token=${encodeURIComponent(token)}`) + break + case 'https-get': + result = await httpRequest( + `${secureOriginBaseUrl}/secure-get?token=${encodeURIComponent(token)}` + ) + break + case 'fetch-post': { + const response = await fetch(`${originBaseUrl}/post?token=${encodeURIComponent(token)}`, { + method: 'POST', + headers: { 'content-type': 'text/plain; charset=utf-8', 'x-manual-case': token }, + body: `manual-request-body:${token}` + }) + result = { + status: response.status, + headers: Object.fromEntries(response.headers), + body: await response.text() + } + break + } + case 'mock-http': + if (backend !== 'legacy') throw new Error('Mock is Legacy-only') + result = await httpRequest(`${originBaseUrl}/mock-http?token=${encodeURIComponent(token)}`) + break + case 'mock-fetch': { + if (backend !== 'legacy') throw new Error('Mock is Legacy-only') + const response = await fetch( + `${originBaseUrl}/mock-fetch?token=${encodeURIComponent(token)}`, + { + method: 'POST', + headers: { 'content-type': 'text/plain', 'x-manual-mock': 'fetch' }, + body: `must-not-reach-origin:${token}` + } + ) + result = { + status: response.status, + headers: Object.fromEntries(response.headers), + body: await response.text() + } + break + } + case 'trace': { + const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' + const tracestate = 'vendor=manual-pr64' + result = await httpRequest(`${originBaseUrl}/trace?token=${encodeURIComponent(token)}`, { + headers: { traceparent, tracestate } + }) + break + } + case 'failed': { + let failure + try { + await httpRequest(`${originBaseUrl}/reset?token=${encodeURIComponent(token)}`) + } catch (error) { + failure = error + } + if (!failure) throw new Error('Reset request unexpectedly completed') + result = { + failed: true, + code: failure instanceof Error && 'code' in failure ? failure.code : null, + message: failure instanceof Error ? failure.message : String(failure) + } + break + } + case 'sse': { + const response = await fetch(`${originBaseUrl}/sse?token=${encodeURIComponent(token)}`) + result = { status: response.status, body: await response.text() } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)) + break + } + case 'websocket': + result = await (backend === 'native' ? nativeWebSocketRoundTrip : webSocketRoundTrip)( + `${originBaseUrl.replace(/^http/, 'ws')}/websocket?token=${encodeURIComponent(token)}`, + token + ) + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)) + break + default: + throw new Error(`Unknown manual scenario: ${scenario}`) + } + + const entry = { + scenario, + token, + result, + originLeakCount: originRecords.filter((record) => record.pathname.startsWith('/mock-')).length + } + scenarioResults.push(entry) + return entry +} + +async function finalizeSession() { + if (finalized) return finalized + await recorder.close() + const exported = await exportHar(sessionDirectory) + const manifest = JSON.parse(await readFile(join(sessionDirectory, 'manifest.json'), 'utf8')) + const events = (await readFile(join(sessionDirectory, 'events.ndjson'), 'utf8')) + .split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)) + const har = exported.har + const requestForPath = (pathname) => + events.find((event) => { + if (event.method !== 'Network.requestWillBeSent') return false + try { + return new URL(event.params.request.url).pathname === pathname + } catch { + return false + } + }) + const failedRequest = requestForPath('/reset') + const failedRequestId = failedRequest?.params.requestId + const failedMethods = failedRequestId + ? events + .filter((event) => event.params?.requestId === failedRequestId) + .map((event) => event.method) + : [] + const webSocketCreated = events.filter((event) => { + if (event.method !== 'Network.webSocketCreated') return false + try { + return new URL(event.params.url).pathname === '/websocket' + } catch { + return false + } + }) + const webSocketRequestIds = new Set(webSocketCreated.map((event) => event.params.requestId)) + const webSocketMethods = events + .filter((event) => webSocketRequestIds.has(event.params?.requestId)) + .map((event) => event.method) + const sseRequest = requestForPath('/sse') + const sseRequestId = sseRequest?.params.requestId + const sseMessageCount = events.filter( + (event) => + event.method === 'Network.eventSourceMessageReceived' && + event.params?.requestId === sseRequestId + ).length + const explicitTraceRecords = originRecords.filter((record) => record.pathname === '/trace') + const untracedRecords = originRecords.filter( + (record) => !['/trace', '/websocket'].includes(record.pathname) + ) + const manualAssertions = { + discovery: discoveryContract, + failedLifecycle: { + requestId: failedRequestId ?? null, + requestWillBeSent: failedMethods.filter((method) => method === 'Network.requestWillBeSent') + .length, + responseReceived: failedMethods.filter((method) => method === 'Network.responseReceived') + .length, + loadingFinished: failedMethods.filter((method) => method === 'Network.loadingFinished') + .length, + loadingFailed: failedMethods.filter((method) => method === 'Network.loadingFailed').length + }, + webSocketBoundary: { + lifecycleCreated: webSocketCreated.length, + lifecycleClosed: webSocketMethods.filter((method) => method === 'Network.webSocketClosed') + .length, + framesSent: webSocketMethods.filter((method) => method === 'Network.webSocketFrameSent') + .length, + framesReceived: webSocketMethods.filter( + (method) => method === 'Network.webSocketFrameReceived' + ).length, + expectedFrameCapture: backend === 'legacy' + }, + sseBoundary: { + requestCaptured: Boolean(sseRequestId), + messageEvents: sseMessageCount, + expectedMessageCapture: backend === 'legacy' + }, + traceBoundary: { + explicitTraceRequests: explicitTraceRecords.length, + explicitTracePreserved: explicitTraceRecords.every( + (record) => + record.traceparent === '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' && + record.tracestate === 'vendor=manual-pr64' + ), + untracedBusinessRequests: untracedRecords.length, + untracedHeadersAbsent: untracedRecords.every( + (record) => record.traceparent === null && record.tracestate === null + ) + } + } + const assertionsPassed = + manualAssertions.failedLifecycle.requestWillBeSent === 1 && + manualAssertions.failedLifecycle.responseReceived === 0 && + manualAssertions.failedLifecycle.loadingFinished === 0 && + manualAssertions.failedLifecycle.loadingFailed === 1 && + manualAssertions.webSocketBoundary.lifecycleCreated === 1 && + manualAssertions.webSocketBoundary.lifecycleClosed === 1 && + (backend === 'legacy' + ? manualAssertions.webSocketBoundary.framesSent === 2 && + manualAssertions.webSocketBoundary.framesReceived === 2 + : manualAssertions.webSocketBoundary.framesSent === 0 && + manualAssertions.webSocketBoundary.framesReceived === 0) && + manualAssertions.sseBoundary.requestCaptured && + (backend === 'legacy' + ? manualAssertions.sseBoundary.messageEvents === 2 + : manualAssertions.sseBoundary.messageEvents === 0) && + manualAssertions.traceBoundary.explicitTraceRequests === 1 && + manualAssertions.traceBoundary.explicitTracePreserved && + manualAssertions.traceBoundary.untracedHeadersAbsent + if (!assertionsPassed) { + throw new Error(`Manual boundary assertions failed: ${JSON.stringify(manualAssertions)}`) + } + const replayableHar = { + ...har, + log: { + ...har.log, + entries: har.log.entries.filter((entry) => { + const url = entry.request?.url + const status = entry.response?.status + return ( + typeof url === 'string' && + url.startsWith(`${originBaseUrl}/`) && + Number.isInteger(status) && + status >= 200 && + status < 400 + ) + }) + } + } + const replayableHarPath = join(runtimeRoot, `${backend}-replayable.har`) + await writeFile(replayableHarPath, `${JSON.stringify(replayableHar, null, 2)}\n`, 'utf8') + const dryRun = await replay(replayableHarPath, { dryRun: true }) + const realReplay = await replay(replayableHarPath, { timeoutMs: 5_000 }) + const traceContexts = Object.values(manifest.traceIndex).flatMap((entry) => entry.spans) + finalized = { + sessionDirectory: redactText(sessionDirectory), + manualAssertions: { passed: true, ...manualAssertions }, + manifest: { + schemaVersion: manifest.schemaVersion, + state: manifest.state, + stats: manifest.stats, + issues: manifest.issues, + traceContexts + }, + har: { + version: har.log.version, + creator: har.log.creator, + entries: har.log.entries.length, + statuses: har.log.entries.map((entry) => entry.response.status), + replayableEntries: replayableHar.log.entries.length + }, + replay: { + dryRun: dryRun.dryRun, + dryRunRequests: dryRun.results.length, + dryRunPassed: dryRun.results.every((entry) => entry.ok), + realRequests: realReplay.results.length, + realPassed: realReplay.results.every((entry) => entry.ok) + }, + originLeakCount: originRecords.filter((record) => record.pathname.startsWith('/mock-')).length + } + await Promise.all([ + writeFile( + join(artifactsRoot, `${backend}-session-manifest.json`), + redactText(await readFile(join(sessionDirectory, 'manifest.json'), 'utf8')), + 'utf8' + ), + writeFile( + join(artifactsRoot, `${backend}-events.ndjson`), + redactText(await readFile(join(sessionDirectory, 'events.ndjson'), 'utf8')), + 'utf8' + ), + writeFile( + join(artifactsRoot, `${backend}-session.har`), + redactText(await readFile(exported.outputPath, 'utf8')), + 'utf8' + ), + writeFile( + join(artifactsRoot, `${backend}-replayable.har`), + redactText(await readFile(replayableHarPath, 'utf8')), + 'utf8' + ), + writeFile( + join(artifactsRoot, `${backend}-finalize-summary.json`), + `${JSON.stringify(finalized, null, 2)}\n`, + 'utf8' + ) + ]) + return finalized +} + +async function endpointClosed(url) { + try { + await fetch(url, { signal: AbortSignal.timeout(750) }) + return false + } catch { + return true + } +} + +async function disposeTarget() { + if (disposed) return disposed + if (!finalized) await finalizeSession() + await registration.dispose() + disposed = { + registrationState: registration.status().state, + discoveryClosed: await endpointClosed(ready.target.discoveryUrl), + targetSocketClosed: await new Promise((resolvePromise) => { + const socket = new WebSocket(ready.target.webSocketDebuggerUrl) + const timer = setTimeout(() => { + socket.terminate() + resolvePromise(false) + }, 750) + socket.once('error', () => { + clearTimeout(timer) + resolvePromise(true) + }) + socket.once('open', () => { + clearTimeout(timer) + socket.close() + resolvePromise(false) + }) + }) + } + await writeFile( + join(artifactsRoot, `${backend}-dispose-summary.json`), + `${JSON.stringify(disposed, null, 2)}\n`, + 'utf8' + ) + return disposed +} + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') +} + +function controlPage(controlUrl) { + const capabilities = Object.entries(ready.capabilities) + .map( + ([name, supported]) => + `
  • ${escapeHtml(name)}${supported ? 'YES' : 'NO'}
  • ` + ) + .join('') + const legacyButtons = + backend === 'legacy' + ? ` + + ` + : '' + return ` + + + + + PR #64 manual evidence — ${backend} + + + +
    +
    PR #64 · Manual acceptance evidence
    +

    Node Network Devtools v2 · ${backend.toUpperCase()}

    +

    Exact packed package, standard CDP discovery, real loopback business traffic.

    +
    +
    +

    Runtime identity READY

    +
    +
    Product commit
    ${escapeHtml(productCommit)}
    +
    Package
    ${escapeHtml(packageManifest.name)}@${escapeHtml(packageManifest.version)}
    +
    Tarball SHA-256
    ${escapeHtml(tarballSha256)}
    +
    Selected backend
    ${escapeHtml(ready.mode)}
    +
    Fallback code
    ${escapeHtml(ready.fallbackReason?.code ?? 'none')}
    +
    Target id
    ${escapeHtml(ready.target.id)}
    +
    Discovery
    ${escapeHtml(ready.target.discoveryUrl)}
    +
    WebSocket
    ${escapeHtml(ready.target.webSocketDebuggerUrl)}
    +
    API references
    fetch=${globalThis.fetch === originalFunctions.fetch}; http.request=${http.request === originalFunctions.httpRequest}; https.request=${https.request === originalFunctions.httpsRequest}
    +
    Discovery contract
    /json/list=${discoveryContract.list.ok}; /json/version=${discoveryContract.version.ok}; /json/protocol.Network=${discoveryContract.protocol.networkDomain}
    +
    +
    + +
    +

    Manual business scenarios

    +
    + + + + + + + + ${legacyButtons} + + +
    +
    + 0 manual scenarios + 0 mock origin leaks + Session recording +
    +
    +
    +

    Latest observed result

    +
    Choose a scenario. Each click causes this Node process to issue real outbound traffic.
    +
    +
    +
    + + +` +} + +const controlSockets = new Set() +let controlUrl +const controlServer = http.createServer(async (request, response) => { + const url = new URL(request.url, controlUrl ?? 'http://127.0.0.1') + try { + if (request.method === 'GET' && url.pathname === '/') { + const body = controlPage(controlUrl) + response.writeHead(200, { + 'content-type': 'text/html; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'cache-control': 'no-store' + }) + response.end(body) + return + } + if (request.method === 'GET' && url.pathname === '/api/status') { + jsonResponse(response, 200, { ready, discovery, frontendUrl, controlUrl }) + return + } + if (request.method === 'POST' && url.pathname.startsWith('/api/run/')) { + jsonResponse(response, 200, await runScenario(url.pathname.slice('/api/run/'.length))) + return + } + if (request.method === 'POST' && url.pathname === '/api/finalize') { + jsonResponse(response, 200, await finalizeSession()) + return + } + if (request.method === 'POST' && url.pathname === '/api/dispose') { + jsonResponse(response, 200, await disposeTarget()) + return + } + jsonResponse(response, 404, { error: 'not found' }) + } catch (error) { + jsonResponse(response, 500, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }) + } +}) +controlServer.on('connection', (socket) => { + controlSockets.add(socket) + socket.once('close', () => controlSockets.delete(socket)) +}) +const controlPort = await listen(controlServer) +controlUrl = `http://127.0.0.1:${controlPort}` + +const runtimeEvidence = { + schemaVersion: 1, + productCommit, + package: { name: packageManifest.name, version: packageManifest.version }, + tarballSha256, + node: process.version, + platform: `${process.platform}-${process.arch}`, + backend, + selectedMode: ready.mode, + fallbackReason: ready.fallbackReason ?? null, + capabilities: ready.capabilities, + target: ready.target, + discovery, + discoveryContract, + frontendUrl, + controlUrl, + originalFunctionsPreserved: { + fetch: globalThis.fetch === originalFunctions.fetch, + httpRequest: http.request === originalFunctions.httpRequest, + httpsRequest: https.request === originalFunctions.httpsRequest + }, + sourceSha256: sha256(await readFile(new URL(import.meta.url))) +} +const publicRuntimeEvidence = redactEvidence(runtimeEvidence) +await writeFile( + join(artifactsRoot, `${backend}-runtime.json`), + `${JSON.stringify(publicRuntimeEvidence, null, 2)}\n`, + 'utf8' +) + +process.stdout.write(`NND_MANUAL_READY ${JSON.stringify(publicRuntimeEvidence)}\n`) + +let shuttingDown = false +async function shutdown() { + if (shuttingDown) return + shuttingDown = true + const errors = [] + if (!finalized) await recorder.close().catch((error) => errors.push(error)) + await registration.dispose().catch((error) => errors.push(error)) + for (const socket of webSocketServer.clients) socket.terminate() + webSocketServer.close() + await closeServer(controlServer, controlSockets).catch((error) => errors.push(error)) + await closeServer(secureOriginServer, secureOriginSockets).catch((error) => errors.push(error)) + await closeServer(originServer, originSockets).catch((error) => errors.push(error)) + if (chromiumProcess.exitCode === null && chromiumProcess.signalCode === null) { + chromiumProcess.kill('SIGTERM') + } + if (errors.length) { + process.stderr.write(`${new AggregateError(errors, 'Manual evidence cleanup failed').stack}\n`) + process.exitCode = 1 + } +} + +process.once('SIGINT', () => void shutdown().finally(() => process.exit())) +process.once('SIGTERM', () => void shutdown().finally(() => process.exit())) diff --git a/output/playwright/pr-64/manual-localhost-cert.pem b/output/playwright/pr-64/manual-localhost-cert.pem new file mode 100644 index 0000000..6238242 --- /dev/null +++ b/output/playwright/pr-64/manual-localhost-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGjCCAgKgAwIBAgIUO15hBEI1W9OrbU3Ad56Vt06/geAwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTI2MDgyODA3MjE0MFoXDTM2MDgy +NTA3MjE0MFowFDESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAymbt8EnBMQrcL3tQDwhaw2OcS29WCDPLw0LoATzXIX+Q +riZbuqtyRzWuzo9JyUKxFAxbiRoG9YNMwVTxSO7Xm/tDxxXt6G1mAaZAMlL470d6 +L0fKPqtnlUKaIn8QuVLoWOqaaVQ1uxs7J/YcyXZlgVl6kVtdM6ZQxLRG+/U1elnN +DlllVU9NhFohkp6ScN5oyHfJ0emKKTMkVaX5I/4qNbqg3vGqCoyhqIJ08fDaZOli +SCrJwNB4Gops59b3raDFeO8D1h4fkxeVgRDMDfWBOd7guIX0FjB54vYSQXQLWf9Y +MLO3dH1jxRq/+CotnL9eSZHbgvyooZVU666s3SRFbwIDAQABo2QwYjAdBgNVHQ4E +FgQUQZZhDStW5svzFv4siWHuxE2iiNwwHwYDVR0jBBgwFoAUQZZhDStW5svzFv4s +iWHuxE2iiNwwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHREECDAGhwR/AAABMA0GCSqG +SIb3DQEBCwUAA4IBAQBcpVVMVTcB4h6vpJV9WQDtnGuI3dqUOwLscK7MhHUPg3ly +hvdDkpTt8H3wXHPr6MVsfGkh9AsJtVFOIxmeSGBTSNdY8lHcaFSAFFfd3eCPmBSE +nYOr7g5A2T1dIzkP5ebYmnT8uFs8bV5F2i7Z4SXX9wCH8Kl0Z6C3XCPKyRV1L+kp +S977JsbnW5JKXWH9q5MdWpgyTkeX19RtSig2jaJUXYLAsbrX218x90VTBlOtV27W +4l118RDarilvn1wX/DuRnVGT0vGOEsQk9S3RPtbZsnXrZYonGZDSZMDLVg+mbEe4 +ViNssMye+vdKCqdHWSZCGwrVbZ/ZglK/PN6AU/b8 +-----END CERTIFICATE----- diff --git a/output/playwright/pr-64/manual-localhost-key.pem b/output/playwright/pr-64/manual-localhost-key.pem new file mode 100644 index 0000000..29b8a5d --- /dev/null +++ b/output/playwright/pr-64/manual-localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDKZu3wScExCtwv +e1APCFrDY5xLb1YIM8vDQugBPNchf5CuJlu6q3JHNa7Oj0nJQrEUDFuJGgb1g0zB +VPFI7teb+0PHFe3obWYBpkAyUvjvR3ovR8o+q2eVQpoifxC5UuhY6pppVDW7Gzsn +9hzJdmWBWXqRW10zplDEtEb79TV6Wc0OWWVVT02EWiGSnpJw3mjId8nR6YopMyRV +pfkj/io1uqDe8aoKjKGognTx8Npk6WJIKsnA0Hgaimzn1vetoMV47wPWHh+TF5WB +EMwN9YE53uC4hfQWMHni9hJBdAtZ/1gws7d0fWPFGr/4Ki2cv15JkduC/KihlVTr +rqzdJEVvAgMBAAECggEAK30hO+OFDKYSOGuNAKXhZqElCHpUKHAEpKBgHD/3031G +13xrcjj7VXyd4kkcaA+Z375l/pfmjeMX32SCcZLMJfo/jmvSUgILjGgt3AJC9ZrM +kJMizANlPsdwOeBGdiNvxAcohWAwKVNJCyWQ+uKjHb9dnBTe+kWOji1UITgHNRHr +kcFg5rm1+y5swALUj8vJDC6a6a4neJ+iRkm1uM0BR0m0s80c0SeQJSi6bdcJAnvw +Z4onJLvnnVdYvk2x3/NIMolT2wKhdeM63LYXrBwbxfHEgwW+ljp3UHVBAgXPJ+9i +WEczCN/erqNlWH01zI3JCUOSAarbbQfpG9Ktq/MkeQKBgQDvSkoKn6z/FFEu6p30 +sI6EC7iivFpwV9/hpPofHz9adAhnCgDSMXWD1KaUYmeGp2ki0PrLEb4bLS9wLCVj +HEc1mg6x4y7VepylxhYQXP7hv/86h48st2aqnPZEBqsMRPNom6U0iAVN2Otjhncd +ov+WsQfJW+uynC3xrddXHvGlOQKBgQDYiTQceALiHWG/rliBamM9X7ergFQFx0UV +AWiPAhZDsYjZQpia3ionjL3TAZ/7YrrRAUfjlGj5hjHDSDKwzGnW4nBly5WpxFTI +C3ZsD0jaSXphAvSDbrsqPOPPJtqupBEoUZcgpqj6ubBf+3KJaZJyTdYVzWx6Ep1Q +1IdqkNCn5wKBgQCTBDiTaGE4Yvox8hHUCTm2ZSEuUrzZ8xNpJhxKTL92yn1zGRFC +EwMZWOhzmDX05BxTOMQtSZxcRBm2OFlMGE6j3yASKPtYS7m8nARoT0qF1plwE2Ni +3NdmEYO4bcRfiGloJuCMctmXZ6PPhQjgG4oewmt/Skt4dD5CE0WPkEJ0+QKBgF+/ +U5afDo9TdKygeCxJkbZKw9mG9iBT+90M8WIDBztJnnyLMRWR1UwFtM4/8rDi+D6A +9XvRXRWw3AJAw4Ff8hD9sbuhaP1XfjGyt9uSaqFzSjTpbJtNdF956zXnNV1x2NBe +O5hJeYDcaPwYII5Ya1Q1zMt5OVVFM+DRCUv62vjTAoGBAI4xYPYed2HI6m0tUAve +Ebf/nCsN028UauPm8LtvflZgjoEks+PqYQ+KU6TnsIuvxvd9ZQpWUj2erWz7C7J3 +7NcU2+VviYSszd0tkXtwWoTNkFS9/pqqk9R3L4D4zm9n5RPjV7PfQPFK7p38CFj6 +YCXILhmtGDiHVQj04rBVSqJs +-----END PRIVATE KEY----- diff --git a/output/playwright/pr-64/manual-native-http2-probe.mjs b/output/playwright/pr-64/manual-native-http2-probe.mjs new file mode 100644 index 0000000..3838f71 --- /dev/null +++ b/output/playwright/pr-64/manual-native-http2-probe.mjs @@ -0,0 +1,84 @@ +import inspector from 'node:inspector' +import http2 from 'node:http2' +import { createRequire } from 'node:module' +import { resolve } from 'node:path' + +const consumerRoot = resolve( + process.env.NND_MANUAL_CONSUMER_ROOT ?? new URL('./consumer', import.meta.url).pathname +) +const manualRequire = createRequire(`${consumerRoot}/consumer.cjs`) +const { register } = manualRequire('node-network-devtools') + +const registration = register({ + mode: 'native', + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } +}) +const ready = await registration.ready +process.stdout.write( + `NND_H2_CAPABILITY ${JSON.stringify({ + node: process.version, + selectedMode: ready.mode, + http2: ready.capabilities.http2 + })}\n` +) + +const cdp = new inspector.Session() +cdp.connect() +for (const method of [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.dataReceived', + 'Network.loadingFinished', + 'Network.loadingFailed' +]) { + cdp.on(method, ({ params }) => { + process.stdout.write( + `NND_H2_EVENT ${JSON.stringify({ + method, + requestId: params.requestId, + url: params.request?.url ?? params.response?.url, + status: params.response?.status, + dataLength: params.dataLength, + errorText: params.errorText + })}\n` + ) + }) +} +await new Promise((resolvePromise, reject) => + cdp.post('Network.enable', {}, (error) => (error ? reject(error) : resolvePromise())) +) + +const server = http2.createServer() +server.on('stream', (stream) => { + stream.respond({ ':status': 200, 'content-type': 'text/plain', 'x-proof': 'h2' }) + stream.end('h2-ok') +}) +await new Promise((resolvePromise, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolvePromise) +}) +const address = server.address() +if (!address || typeof address === 'string') throw new Error('HTTP/2 server has no port') + +const client = http2.connect(`http://127.0.0.1:${address.port}`) +const request = client.request({ + ':method': 'GET', + ':path': '/native-h2?token=manual-pr64' +}) +let body = '' +request.setEncoding('utf8') +request.on('data', (chunk) => { + body += chunk +}) +request.end() +await new Promise((resolvePromise, reject) => { + request.once('end', resolvePromise) + request.once('error', reject) +}) +process.stdout.write(`NND_H2_PASS ${JSON.stringify({ node: process.version, body })}\n`) + +client.close() +await new Promise((resolvePromise) => server.close(resolvePromise)) +cdp.disconnect() +await registration.dispose() diff --git a/output/playwright/pr-64/manual-native-mock-conflict.mjs b/output/playwright/pr-64/manual-native-mock-conflict.mjs new file mode 100644 index 0000000..ba02896 --- /dev/null +++ b/output/playwright/pr-64/manual-native-mock-conflict.mjs @@ -0,0 +1,30 @@ +const packageEntry = new URL( + './consumer/node_modules/node-network-devtools/dist/index.mjs', + import.meta.url +) +const { register } = await import(packageEntry.href) + +try { + register({ + mode: 'native', + legacy: { + mock: [ + { + match: { url: 'http://127.0.0.1/*' }, + response: { body: 'must-not-register' } + } + ] + } + }) + throw new Error('Native plus Mock unexpectedly registered') +} catch (error) { + const result = { + name: error?.name, + code: error?.code, + message: error?.message, + details: error?.details + } + + console.log(JSON.stringify(result, null, 2)) + if (result.code !== 'NND_NATIVE_MOCK_CONFLICT') process.exitCode = 1 +} diff --git a/output/playwright/pr-64/screenshots/MT-01-cli-version-doctor-native.png b/output/playwright/pr-64/screenshots/MT-01-cli-version-doctor-native.png new file mode 100644 index 0000000..91b22cb Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-01-cli-version-doctor-native.png differ diff --git a/output/playwright/pr-64/screenshots/MT-02-cli-zero-code-and-conflict.png b/output/playwright/pr-64/screenshots/MT-02-cli-zero-code-and-conflict.png new file mode 100644 index 0000000..58b6148 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-02-cli-zero-code-and-conflict.png differ diff --git a/output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png b/output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png new file mode 100644 index 0000000..60c9089 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-03-native-runtime-and-capabilities.png differ diff --git a/output/playwright/pr-64/screenshots/MT-04-native-version.png b/output/playwright/pr-64/screenshots/MT-04-native-version.png new file mode 100644 index 0000000..78eea5b Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-04-native-version.png differ diff --git a/output/playwright/pr-64/screenshots/MT-05-native-devtools-network-list.png b/output/playwright/pr-64/screenshots/MT-05-native-devtools-network-list.png new file mode 100644 index 0000000..f58a1fb Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-05-native-devtools-network-list.png differ diff --git a/output/playwright/pr-64/screenshots/MT-05-native-fetch-headers.png b/output/playwright/pr-64/screenshots/MT-05-native-fetch-headers.png new file mode 100644 index 0000000..e46975b Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-05-native-fetch-headers.png differ diff --git a/output/playwright/pr-64/screenshots/MT-05-native-fetch-response.png b/output/playwright/pr-64/screenshots/MT-05-native-fetch-response.png new file mode 100644 index 0000000..4cbbee5 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-05-native-fetch-response.png differ diff --git a/output/playwright/pr-64/screenshots/MT-06-native-devtools-reload-reconnect.png b/output/playwright/pr-64/screenshots/MT-06-native-devtools-reload-reconnect.png new file mode 100644 index 0000000..74bc364 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-06-native-devtools-reload-reconnect.png differ diff --git a/output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png b/output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png new file mode 100644 index 0000000..209eafb Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-07-legacy-runtime-and-capabilities.png differ diff --git a/output/playwright/pr-64/screenshots/MT-08-legacy-version.png b/output/playwright/pr-64/screenshots/MT-08-legacy-version.png new file mode 100644 index 0000000..309b690 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-08-legacy-version.png differ diff --git a/output/playwright/pr-64/screenshots/MT-09-legacy-devtools-network-list.png b/output/playwright/pr-64/screenshots/MT-09-legacy-devtools-network-list.png new file mode 100644 index 0000000..9800476 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-09-legacy-devtools-network-list.png differ diff --git a/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-payload.png b/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-payload.png new file mode 100644 index 0000000..7a38c8d Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-payload.png differ diff --git a/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-response.png b/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-response.png new file mode 100644 index 0000000..1fc8e69 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-09-legacy-fetch-response.png differ diff --git a/output/playwright/pr-64/screenshots/MT-10-legacy-mock-fetch-response.png b/output/playwright/pr-64/screenshots/MT-10-legacy-mock-fetch-response.png new file mode 100644 index 0000000..1f24db7 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-10-legacy-mock-fetch-response.png differ diff --git a/output/playwright/pr-64/screenshots/MT-10-legacy-mock-http-response.png b/output/playwright/pr-64/screenshots/MT-10-legacy-mock-http-response.png new file mode 100644 index 0000000..034ae8c Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-10-legacy-mock-http-response.png differ diff --git a/output/playwright/pr-64/screenshots/MT-11-legacy-sse-eventstream.png b/output/playwright/pr-64/screenshots/MT-11-legacy-sse-eventstream.png new file mode 100644 index 0000000..3af8eb9 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-11-legacy-sse-eventstream.png differ diff --git a/output/playwright/pr-64/screenshots/MT-11-legacy-websocket-messages.png b/output/playwright/pr-64/screenshots/MT-11-legacy-websocket-messages.png new file mode 100644 index 0000000..fa80783 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-11-legacy-websocket-messages.png differ diff --git a/output/playwright/pr-64/screenshots/MT-11-native-websocket-lifecycle.png b/output/playwright/pr-64/screenshots/MT-11-native-websocket-lifecycle.png new file mode 100644 index 0000000..51e33d8 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-11-native-websocket-lifecycle.png differ diff --git a/output/playwright/pr-64/screenshots/MT-12-legacy-session-har-replay-trace.png b/output/playwright/pr-64/screenshots/MT-12-legacy-session-har-replay-trace.png new file mode 100644 index 0000000..f18ee84 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-12-legacy-session-har-replay-trace.png differ diff --git a/output/playwright/pr-64/screenshots/MT-12-native-session-har-replay-trace.png b/output/playwright/pr-64/screenshots/MT-12-native-session-har-replay-trace.png new file mode 100644 index 0000000..aca3cc0 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-12-native-session-har-replay-trace.png differ diff --git a/output/playwright/pr-64/screenshots/MT-13-legacy-boundary-assertions.png b/output/playwright/pr-64/screenshots/MT-13-legacy-boundary-assertions.png new file mode 100644 index 0000000..5891881 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-13-legacy-boundary-assertions.png differ diff --git a/output/playwright/pr-64/screenshots/MT-13-legacy-failed-request.png b/output/playwright/pr-64/screenshots/MT-13-legacy-failed-request.png new file mode 100644 index 0000000..08cf000 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-13-legacy-failed-request.png differ diff --git a/output/playwright/pr-64/screenshots/MT-13-native-boundary-assertions.png b/output/playwright/pr-64/screenshots/MT-13-native-boundary-assertions.png new file mode 100644 index 0000000..49313a4 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-13-native-boundary-assertions.png differ diff --git a/output/playwright/pr-64/screenshots/MT-13-native-failed-request.png b/output/playwright/pr-64/screenshots/MT-13-native-failed-request.png new file mode 100644 index 0000000..f649ecf Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-13-native-failed-request.png differ diff --git a/output/playwright/pr-64/screenshots/MT-14-legacy-dispose-cleanup.png b/output/playwright/pr-64/screenshots/MT-14-legacy-dispose-cleanup.png new file mode 100644 index 0000000..eb11045 Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-14-legacy-dispose-cleanup.png differ diff --git a/output/playwright/pr-64/screenshots/MT-14-native-dispose-cleanup.png b/output/playwright/pr-64/screenshots/MT-14-native-dispose-cleanup.png new file mode 100644 index 0000000..d2a338f Binary files /dev/null and b/output/playwright/pr-64/screenshots/MT-14-native-dispose-cleanup.png differ diff --git a/package.json b/package.json index 1333ecb..f88d9ab 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,13 @@ "private": true, "scripts": { "build": "turbo build", + "test": "pnpm --filter node-network-devtools test", + "test:e2e:native": "pnpm --filter node-network-devtools test:e2e:native", + "test:e2e:legacy": "pnpm --filter node-network-devtools test:e2e:legacy", + "test:e2e:enhancements": "pnpm --filter node-network-devtools test:e2e:enhancements", + "test:e2e:cli": "pnpm --filter node-network-devtools test:e2e:cli", + "test:e2e:frontend": "pnpm --filter node-network-devtools test:e2e:frontend", + "quality": "pnpm --filter node-network-devtools test:ci", "dev": "turbo dev", "lint": "turbo lint", "start": "turbo start", @@ -26,6 +33,6 @@ }, "packageManager": "pnpm@9.12.2", "engines": { - "node": ">=18" + "node": ">=18.18" } } diff --git a/packages/network-debugger/LICENSE b/packages/network-debugger/LICENSE new file mode 100644 index 0000000..f6126a2 --- /dev/null +++ b/packages/network-debugger/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 bugyaluwang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/network-debugger/README.md b/packages/network-debugger/README.md index d6c366a..6d6c971 100644 --- a/packages/network-debugger/README.md +++ b/packages/network-debugger/README.md @@ -1,82 +1,213 @@ -
    -

    - -
    Node Network Devtools

    - -

    🔮 Use chrome network devtool to debugger nodejs

    -

    🦎 Similar web crawler experience to browsers

    -

    ⚙️ Powered by CDP

    -

    -

    - NodeJs - Chrome - TypeScript -

    -

    - -
    - ---- - -English | [简体中文](README-zh_CN.md) - -## 📖 Introduction - -As you can see, the node program opened with the `--inspect` option does not support network tags because it does not proxy user requests. -Node network devtools is designed to address this issue by allowing you to debug requests made by nodejs using the network tab of Chrome devtools, making the debugging process equivalent to a web crawler experience in the browser. - -Node v22.6.0 experimentally supports network debugging. This library supports use before node v22.6.0, but the specific supported versions are unknown. - -## 🎮 Features - -- [x] HTTP/HTTPS - - [x] req/res headers - - [x] payload - - [x] json str response body - - [x] binary response body - - [x] stack follow - - [x] show stack - - [x] click to jump - - [x] base - - [x] Sourcemap -- [x] WebSocket - - [x] messages - - [x] payload - - [x] headers -- [ ] Compatibility - - [x] commonjs - - [x] esmodule - - [ ] Bun - - [ ] Deno -- Maybe nice to PR to Deno -- [ ] Undici - - [ ] undici.request - - [x] undici.fetch - -## 👀 Preview - -![img](https://github.com/GrinZero/node-network-devtools/assets/70185413/5338d8f2-bb54-46fd-b243-a7a5b4af3031) - -## 📦 Quick Start - -### 1. Install +# Node Network Devtools + +Inspect outbound Node.js network traffic with the standard Chrome DevTools +Network panel. Version 2 provides two complete, mutually exclusive backends: + +- **Native** connects DevTools directly to Node's experimental Network Inspector. +- **Legacy** captures outbound APIs in-process and exposes a standard CDP target + through an isolated child-process bridge. + +The runtime owns a debuggable target, not a Chrome process. Opening DevTools is +explicit and disposal never kills a browser. + +## Requirements + +- Node.js `>=18.18`. +- Native mode requires a Node release with + `--experimental-network-inspection`; use `nnd doctor` to inspect the exact + runtime capability matrix. +- Node 18 and 20 are retained as compatibility lanes even though they are EOL. +- `undici@^6` is a peer dependency so the opt-in Legacy hook observes the same + package instance as the application. Package managers that disable automatic + peer installation must install it explicitly, even when that hook stays off. + +## Install ```bash -# npm -npm install node-network-devtools -D -# or pnpm -pnpm add node-network-devtools -D -# or yarn -yarn add node-network-devtools -D +npm install --save-dev node-network-devtools ``` -### 2. Usage +## Zero-code CLI + +```bash +# Start a target and wait for a debugger before running the entry point. +npx nnd dev src/app.js -Just add the following code to the entry file of your project. +# Open the target explicitly. +npx nnd dev --open src/app.js -```typescript +# Start immediately, use tsx, or force a backend. +npx nnd dev --no-wait --runner tsx --mode legacy src/app.ts + +# Machine-readable environment and selection diagnostics. +npx nnd doctor --json +``` + +`nnd dev` supports CJS, ESM, tsx, compiled Nest-style applications, application +arguments, signals, and watch restarts without a source-code registration edit. + +## Library API + +```ts import { register } from 'node-network-devtools' -process.env.NODE_ENV === 'development' && register() +const registration = register({ + mode: 'auto', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } +}) + +const ready = await registration.ready +console.log(ready.mode, ready.target, ready.capabilities, ready.fallbackReason) + +await registration.openDevtools() +await registration.dispose() +``` + +The returned handle remains callable for v1 compatibility: + +```ts +const unregister = register() +unregister() +``` + +Equal repeated registrations return the same handle. A conflicting concurrent +registration fails with `NND_ALREADY_REGISTERED`. + +## Backend capabilities + +| Capability | Native | Legacy | +| ---------------------- | ----------------------- | ------ | +| HTTP / HTTPS lifecycle | Yes | Yes | +| Fetch lifecycle | Runtime-dependent | Yes | +| HTTP/2 | Node 22.20+ (22.x only) | No | +| Response bodies | Runtime-dependent | Yes | +| Request bodies | Not advertised | Yes | +| WebSocket lifecycle | Runtime-dependent | Yes | +| WebSocket frames | No | Yes | +| SSE messages | No | Yes | +| Initiator stack | Yes | Yes | +| Request/response Mock | No | Yes | + +Native capabilities are probed from the running Node version and Inspector API; +the package does not pretend missing upstream features exist. Forced Native +fails when requirements cannot be met. Auto prefers a proven Native baseline +and otherwise exposes a structured Legacy fallback reason. + +Node 22 can expose Native HTTP response bodies while returning an empty Fetch +body. Because `responseBody` covers all advertised transports, the package +conservatively reports that capability as false on Node 22 and enables it only +on the verified Node 24+ baseline. + +Native HTTP/2 is conservatively allowlisted only for Node 22.20+ releases in +the 22.x line. A non-empty h2c lifecycle passed on Node 22.22.3, while consuming +a non-empty response with `setEncoding()` crashes the upstream experimental +Inspector on Node 24.16.0 and 26.8.1 with `Missing dataLength`. Other and future +majors remain reported as unsupported until independently verified. Legacy does +not capture HTTP/2. + +Legacy interception can be narrowed: + +```ts +register({ + mode: 'legacy', + legacy: { + intercept: { normal: true, fetch: true, undici: { fetch: true } } + } +}) +``` + +## Session, HAR, and Replay + +Record either backend to a portable Session directory and optionally export HAR +during disposal: + +```ts +const registration = register({ + session: { + directory: '.nnd/sessions/run-001', + har: true + } +}) + +const ready = await registration.ready +console.log(ready.session) +await registration.dispose() +``` + +The directory contains `manifest.json`, `events.ndjson`, and external files in +`bodies/`. Existing W3C `traceparent`/`tracestate` headers are correlated in the +manifest and HAR without injecting or modifying tracing headers. + +Session APIs are also exported directly: + +```ts +import { buildHar, exportHar, replay, SessionRecorder } from 'node-network-devtools' + +await exportHar('.nnd/sessions/run-001', 'capture.har') +const plan = await replay('capture.har', { dryRun: true }) +const result = await replay('.nnd/sessions/run-001') +``` + +Replay accepts a Session directory, HAR file, or HAR object. It only reissues +HTTP(S), uses manual redirect handling, and removes hop-by-hop plus runtime-owned +`Host`/`Content-Length` headers. The CLI exposes the same operation: + +```bash +npx nnd replay --dry-run --json capture.har +npx nnd replay --stop-on-error .nnd/sessions/run-001 +``` + +## Legacy-only Mock + +```ts +const registration = register({ + mode: 'auto', + legacy: { + mock: [ + { + id: 'fixture', + match: { + url: 'https://api.example.test/v1/*', + method: 'POST', + headers: { 'x-test-mode': 'mock' } + }, + response: { + status: 201, + headers: { 'content-type': 'application/json' }, + body: '{"mocked":true}' + } + } + ] + } +}) +``` + +URL matchers support `*` globs. Use `bodyBase64` for binary responses. Auto +selects Legacy with diagnostic `NND_AUTO_LEGACY_MOCK_REQUIRED`; forcing Native +with rules fails explicitly with `NND_NATIVE_MOCK_CONFLICT`. Mocked HTTP, HTTPS, +global Fetch, and opted-in `undici.fetch` responses still traverse the normal +Network capture lifecycle. + +## Configuration file + +`nnd.config.mjs`, `nnd.config.cjs`, and `nnd.config.json` are discovered in the +working directory. Precedence is CLI > `NND_*` environment > config file > +defaults. + +```js +export default { + mode: 'auto', + open: false, + wait: true, + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: ['responseBody'], + session: { directory: '.nnd/sessions/local', har: true }, + legacy: { serverPort: 0 } +} ``` -![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2FGrinZero%2Fnode-network-devtools&labelColor=%237fa1f7&countColor=%23697689) +See the repository's +[v2 migration guide](https://github.com/GrinZero/node-network-devtools/blob/main/docs/v2-migration.md) +for old-option mappings and release compatibility details. diff --git a/packages/network-debugger/package.json b/packages/network-debugger/package.json index 0e5aeca..17d9181 100644 --- a/packages/network-debugger/package.json +++ b/packages/network-debugger/package.json @@ -1,28 +1,54 @@ { "name": "node-network-devtools", - "version": "1.0.30", + "version": "2.0.0", "description": "Inspecting Node.js's Network with Chrome DevTools", "homepage": "https://grinzero.github.io/node-network-devtools/", "main": "./dist/index.js", "module": "./dist/index.mjs", - "types": "./dist/src/index.d.ts", + "types": "./dist/index.d.ts", + "bin": { + "nnd": "./dist/cli.mjs", + "node-network-devtools": "./dist/cli.mjs" + }, "exports": { ".": { - "types": "./dist/src/index.d.ts", + "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" }, "./dev": { - "types": "./src/index.ts", - "import": "./src/index.ts", - "require": "./src/index.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./register": { + "types": "./dist/preload/register.d.ts", + "import": "./dist/register.mjs", + "default": "./dist/register.mjs" + }, + "./preload": { + "types": "./dist/preload/register.d.ts", + "import": "./dist/register.mjs", + "default": "./dist/register.mjs" + }, + "./config": { + "types": "./dist/config/index.d.ts", + "import": "./dist/config.mjs", + "require": "./dist/config.js" } }, "scripts": { - "test": "vitest", + "test": "vitest run", + "test:unit": "vitest run", + "test:e2e:native": "node --test test/e2e/native-protocol.test.mjs", + "test:e2e:legacy": "node --test test/e2e/legacy/legacy-protocol.test.mjs", + "test:e2e:enhancements": "node --test test/e2e/enhancements/enhancements.test.mjs", + "test:e2e:cli": "node --test test/e2e/cli/cli.test.mjs", + "test:e2e:frontend": "playwright test -c test/e2e/frontend/playwright.config.mjs", + "test:ci": "pnpm run test:unit && pnpm run build && pnpm run test:e2e:native && pnpm run test:e2e:legacy && pnpm run test:e2e:enhancements && pnpm run test:e2e:cli && pnpm run test:e2e:frontend", "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", - "build": "vite build", + "build": "vite build && vite build --config vite.preload.config.ts", "dev": "vite build --watch --mode development" }, "keywords": [ @@ -47,7 +73,11 @@ }, "author": "bugyaluwang (https://github.com/GrinZero)", "license": "MIT", + "engines": { + "node": ">=18.18" + }, "devDependencies": { + "@playwright/test": "1.62.1", "@types/node": "^20.11.0", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.14.0", @@ -57,6 +87,7 @@ "fast-check": "^4.5.3", "memfs": "^4.56.10", "tslib": "2.6.2", + "tsx": "4.23.12", "vite": "^5.2.10", "vite-plugin-dts": "^3.8.3", "vitest": "^4.0.18" @@ -64,7 +95,6 @@ "dependencies": { "bufferutil": "^4.0.9", "iconv-lite": "^0.7.0", - "inspector": "^0.5.0", "open": "^8.4.2", "ws": "^8.17.1" }, diff --git a/packages/network-debugger/src/adapters/legacy/index.test.ts b/packages/network-debugger/src/adapters/legacy/index.test.ts new file mode 100644 index 0000000..41c8db8 --- /dev/null +++ b/packages/network-debugger/src/adapters/legacy/index.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vitest' +import { LEGACY_CAPABILITIES, LegacyAdapter } from '.' + +describe('LegacyAdapter capabilities', () => { + test('advertises the tested default Legacy feature set', () => { + expect(new LegacyAdapter().probe().capabilities).toEqual(LEGACY_CAPABILITIES) + }) + + test('does not advertise transports explicitly disabled by interception config', () => { + expect( + new LegacyAdapter({ intercept: { normal: false, fetch: true } }).probe().capabilities + ).toMatchObject({ + http: false, + https: false, + fetch: true, + websocketLifecycle: false, + websocketFrames: false, + sseMessages: true, + responseBody: true + }) + + expect( + new LegacyAdapter({ intercept: { normal: false, fetch: false } }).probe().capabilities + ).toMatchObject({ + http: false, + https: false, + fetch: false, + responseBody: false, + requestBody: false, + sseMessages: false, + initiator: false + }) + }) +}) diff --git a/packages/network-debugger/src/adapters/legacy/index.ts b/packages/network-debugger/src/adapters/legacy/index.ts new file mode 100644 index 0000000..c2802a3 --- /dev/null +++ b/packages/network-debugger/src/adapters/legacy/index.ts @@ -0,0 +1,195 @@ +import http from 'http' +import https from 'https' +import { syncBuiltinESMExports } from 'node:module' +import { type InterceptOptions } from '../../common' +import { MainProcess } from '../../core/fork' +import { proxyFetch } from '../../core/fetch' +import { getProxyFactory, requestProxyFactory, type RequestFn } from '../../core/request' +import { undiciFetchProxy } from '../../core/undici' +import { mockableRequestHandler, type LegacyMockRule } from '../../mock' +import { generateHash } from '../../utils' +import type { + AdapterProbe, + AdapterSession, + AdapterStartOptions, + CapabilityMap, + DebugAdapter, + Diagnostic +} from '../types' + +export const LEGACY_CAPABILITIES: CapabilityMap = Object.freeze({ + http: true, + https: true, + fetch: true, + http2: false, + responseBody: true, + requestBody: true, + websocketLifecycle: true, + websocketFrames: true, + sseMessages: true, + initiator: true +}) + +export interface LegacyAdapterOptions { + port?: number + serverPort?: number + autoOpenDevtool?: boolean + intercept?: InterceptOptions + mock?: readonly LegacyMockRule[] + diagnostics?: readonly Diagnostic[] +} + +function capabilitiesFor(intercept: InterceptOptions = {}): CapabilityMap { + const normal = intercept.normal !== false + const fetch = intercept.fetch !== false || Boolean(intercept.undici && intercept.undici.fetch) + const anyRequest = normal || fetch + return Object.freeze({ + http: normal, + https: normal, + fetch, + http2: false, + responseBody: anyRequest, + requestBody: anyRequest, + websocketLifecycle: normal, + websocketFrames: normal, + sseMessages: fetch, + initiator: anyRequest + }) +} + +/** + * Compatibility wrapper around the original capture path. + * + * All application capture patches live here, while a child-process IPC bridge + * owns the project CDP target. Native sessions therefore cannot accidentally + * activate any Legacy interception. + */ +export class LegacyAdapter implements DebugAdapter { + readonly kind = 'legacy' as const + + constructor(private readonly options: LegacyAdapterOptions = {}) {} + + probe(_options?: AdapterStartOptions): AdapterProbe { + return { + kind: this.kind, + available: true, + autoSelectable: true, + capabilities: capabilitiesFor(this.options.intercept), + diagnostics: [] + } + } + + async start(_options?: AdapterStartOptions): Promise { + const port = this.options.port + const serverPort = this.options.serverPort ?? 0 + const autoOpenDevtool = this.options.autoOpenDevtool ?? false + const intercept = this.options.intercept ?? {} + const mockRules = this.options.mock ?? [] + const { + fetch: interceptFetch = true, + normal: interceptNormal = true, + undici: interceptUndici = false + } = intercept + const interceptUndiciFetch = Boolean(interceptUndici && interceptUndici.fetch) + const capabilities = capabilitiesFor(intercept) + const key = generateHash(JSON.stringify({ port, serverPort, autoOpenDevtool })) + const mainProcess = new MainProcess({ port, serverPort, autoOpenDevtool, key }) + + const unsetFetchProxy = interceptFetch + ? mockRules.length > 0 + ? proxyFetch(mainProcess, mockRules) + : proxyFetch(mainProcess) + : undefined + const originalRequests = new Map< + typeof http | typeof https, + { request: RequestFn; get: RequestFn; proxyRequest: RequestFn; proxyGet: RequestFn } + >() + const agents = [http, https] as const + + if (interceptNormal) { + for (const agent of agents) { + const request = agent.request as RequestFn + const get = agent.get as RequestFn + const requestHandler = + mockRules.length > 0 + ? mockableRequestHandler(request, agent === https, mockRules) + : request + const proxyRequest = requestProxyFactory.call( + agent, + requestHandler, + agent === https, + mainProcess + ) + const proxyGet = getProxyFactory(proxyRequest) + originalRequests.set(agent, { request, get, proxyRequest, proxyGet }) + agent.request = proxyRequest as typeof agent.request + agent.get = proxyGet as typeof agent.get + } + syncBuiltinESMExports() + } + + const unsetUndiciFetch = interceptUndiciFetch + ? mockRules.length > 0 + ? undiciFetchProxy(mainProcess, mockRules) + : undiciFetchProxy(mainProcess) + : undefined + let disposed = false + + const restoreCapture = () => { + unsetFetchProxy?.() + if (interceptNormal) { + for (const agent of agents) { + const original = originalRequests.get(agent) + if (!original) continue + if (agent.request === original.proxyRequest) { + agent.request = original.request as typeof agent.request + } + if (agent.get === original.proxyGet) { + agent.get = original.get as typeof agent.get + } + } + originalRequests.clear() + syncBuiltinESMExports() + } + unsetUndiciFetch?.() + } + + let target + try { + target = await mainProcess.ready + } catch (error) { + restoreCapture() + await mainProcess.dispose().catch(() => {}) + throw error + } + + const diagnostics: Diagnostic[] = [...(this.options.diagnostics ?? [])] + if (port !== undefined) { + diagnostics.push({ + code: 'NND_LEGACY_BRIDGE_PORT_DEPRECATED', + level: 'warn', + message: 'Legacy bridge port is no longer used because capture now uses child-process IPC.', + hint: 'Remove legacy.port (or the deprecated top-level port option). Use legacy.serverPort only to pin the CDP target port.' + }) + } + + return { + kind: this.kind, + capabilities, + diagnostics, + target, + onDiagnostic(listener: (diagnostic: Diagnostic) => void) { + return mainProcess.onDiagnostic(listener) + }, + onFailure(listener: (error: Error) => void) { + return mainProcess.onFailure(listener) + }, + async dispose() { + if (disposed) return + disposed = true + restoreCapture() + await mainProcess.dispose() + } + } + } +} diff --git a/packages/network-debugger/src/adapters/node-native/capability.test.ts b/packages/network-debugger/src/adapters/node-native/capability.test.ts new file mode 100644 index 0000000..28cb693 --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/capability.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test, vi } from 'vitest' +import { + NATIVE_CAPABILITIES, + getMissingCapabilities, + getNativeCapabilities, + hasNativeInspectionFlag, + isNativeAutoBaseline, + parseNodeVersion, + supportsNativeNetworkInspection, + type NativeNetworkApi +} from './capability' + +const lifecycleNetwork = (): NativeNetworkApi => ({ + requestWillBeSent: vi.fn(), + responseReceived: vi.fn(), + loadingFinished: vi.fn(), + loadingFailed: vi.fn() +}) + +describe('native capability detection', () => { + test('publishes a conservative maximum capability set', () => { + expect(NATIVE_CAPABILITIES).toMatchObject({ + http: true, + https: true, + fetch: true, + http2: true, + responseBody: true, + requestBody: false, + websocketLifecycle: true, + websocketFrames: false, + sseMessages: false, + initiator: true + }) + }) + + test.each([ + ['20.17.0', false], + ['20.18.0', true], + ['21.7.3', false], + ['22.5.0', false], + ['22.6.0', true], + ['23.0.0', true], + ['24.0.0', true] + ])('detects native inspection runtime support for Node %s', (text, expected) => { + expect(supportsNativeNetworkInspection(parseNodeVersion(text))).toBe(expected) + }) + + test.each([ + ['24.6.0', false], + ['24.7.0', true], + ['25.0.0', true], + ['22.22.0', false] + ])('uses a stricter Auto baseline for Node %s', (text, expected) => { + expect(isNativeAutoBaseline(parseNodeVersion(text))).toBe(expected) + }) + + test('rejects malformed versions', () => { + expect(parseNodeVersion('nightly')).toBeNull() + }) + + test('detects the experimental flag without accepting lookalikes', () => { + expect(hasNativeInspectionFlag(['--experimental-network-inspection'])).toBe(true) + expect(hasNativeInspectionFlag(['--experimental-network-inspection=true'])).toBe(true) + expect(hasNativeInspectionFlag(['--experimental-network-inspection-extra'])).toBe(false) + }) + + test('derives optional capabilities from runtime methods and versions', () => { + const network: NativeNetworkApi = { + ...lifecycleNetwork(), + dataReceived: vi.fn(), + dataSent: vi.fn(), + webSocketCreated: vi.fn(), + webSocketHandshakeResponseReceived: vi.fn(), + webSocketClosed: vi.fn() + } + const capabilities = getNativeCapabilities(parseNodeVersion('24.8.0'), network) + + expect(capabilities).toMatchObject({ + http: true, + https: true, + fetch: true, + http2: false, + responseBody: true, + requestBody: false, + websocketLifecycle: true, + websocketFrames: false, + sseMessages: false, + initiator: true + }) + }) + + test.each([ + ['22.19.0', false], + ['22.20.0', true], + ['22.22.3', true], + ['23.11.1', false], + ['24.16.0', false], + ['25.7.0', false], + ['26.8.1', false], + ['99.0.0', false] + ])('uses the explicit Native HTTP/2 allowlist for Node %s', (text, expected) => { + const capabilities = getNativeCapabilities(parseNodeVersion(text), lifecycleNetwork()) + expect(capabilities.http2).toBe(expected) + }) + + test('does not infer HTTP request-body support from dataSent', () => { + const capabilities = getNativeCapabilities(parseNodeVersion('26.1.0'), { + ...lifecycleNetwork(), + dataSent: vi.fn() + }) + expect(capabilities.requestBody).toBe(false) + }) + + test('does not advertise cross-transport response bodies on Node 22', () => { + const capabilities = getNativeCapabilities(parseNodeVersion('22.22.3'), { + ...lifecycleNetwork(), + dataReceived: vi.fn() + }) + expect(capabilities).toMatchObject({ fetch: true, responseBody: false }) + }) + + test('turns protocol capabilities off when lifecycle methods are incomplete', () => { + const capabilities = getNativeCapabilities(parseNodeVersion('26.1.0'), { + requestWillBeSent: vi.fn() + }) + expect(capabilities.http).toBe(false) + expect(capabilities.fetch).toBe(false) + expect(capabilities.initiator).toBe(false) + }) + + test('reports required capabilities that are unavailable', () => { + const capabilities = getNativeCapabilities(parseNodeVersion('24.7.0'), lifecycleNetwork()) + expect( + getMissingCapabilities(capabilities, ['http', 'responseBody', 'websocketFrames']) + ).toEqual(['responseBody', 'websocketFrames']) + }) +}) diff --git a/packages/network-debugger/src/adapters/node-native/capability.ts b/packages/network-debugger/src/adapters/node-native/capability.ts new file mode 100644 index 0000000..bd94bfa --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/capability.ts @@ -0,0 +1,161 @@ +import type { CapabilityMap, NetworkCapability } from '../types' + +export const NATIVE_NETWORK_INSPECTION_FLAG = '--experimental-network-inspection' + +/** + * The maximum capability set currently exposed by Node's native inspector. + * Runtime probing may turn individual capabilities off for older Node releases. + * + * `requestBody` is intentionally false: Node's native HTTP/1 inspector does not + * yet provide request bodies consistently, so the adapter must not advertise a + * capability which only works for some transports. + */ +export const NATIVE_CAPABILITIES: CapabilityMap = Object.freeze({ + http: true, + https: true, + fetch: true, + http2: true, + responseBody: true, + requestBody: false, + websocketLifecycle: true, + websocketFrames: false, + sseMessages: false, + initiator: true +}) + +export const REQUIRED_NATIVE_NETWORK_METHODS = [ + 'requestWillBeSent', + 'responseReceived', + 'loadingFinished', + 'loadingFailed' +] as const + +export const OPTIONAL_NATIVE_NETWORK_METHODS = [ + 'dataReceived', + 'dataSent', + 'webSocketCreated', + 'webSocketHandshakeResponseReceived', + 'webSocketClosed' +] as const + +export type NativeNetworkMethod = + | (typeof REQUIRED_NATIVE_NETWORK_METHODS)[number] + | (typeof OPTIONAL_NATIVE_NETWORK_METHODS)[number] + +export type NativeNetworkApi = Partial unknown>> + +export interface NodeVersion { + major: number + minor: number + patch: number +} + +export function parseNodeVersion(version: string): NodeVersion | null { + const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)/.exec(version.trim()) + if (!match) return null + + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]) + } +} + +function atLeast(version: NodeVersion, major: number, minor: number, patch = 0) { + if (version.major !== major) return version.major > major + if (version.minor !== minor) return version.minor > minor + return version.patch >= patch +} + +/** The flag first shipped in Node 20.18 and Node 22.6. */ +export function supportsNativeNetworkInspection(version: NodeVersion | null) { + if (!version) return false + if (version.major === 20) return atLeast(version, 20, 18) + if (version.major === 21) return false + if (version.major === 22) return atLeast(version, 22, 6) + return version.major >= 23 +} + +/** + * Auto selection deliberately has a stricter, proven baseline than explicit + * Native selection. Older supported releases remain available when requested + * explicitly, but Auto should prefer Legacy there. + */ +export function isNativeAutoBaseline(version: NodeVersion | null) { + if (!version) return false + if (version.major > 24) return true + return version.major === 24 && atLeast(version, 24, 7) +} + +function supportsNativeFetch(version: NodeVersion | null) { + if (!version) return false + if (version.major === 22) return atLeast(version, 22, 14) + if (version.major === 23) return atLeast(version, 23, 7) + return version.major >= 24 +} + +function supportsNativeHttp2(version: NodeVersion | null) { + if (!version) return false + + // Keep this as an explicit allowlist. A non-empty h2c lifecycle is verified on + // Node 22.20+, while newer majors have regressed in the experimental Inspector. + // Future majors must be verified independently instead of inheriting support. + return version.major === 22 && atLeast(version, 22, 20) +} + +/** + * Node 22 exposes dataReceived but its Fetch getResponseBody result can be + * empty. The public capability spans every advertised transport, so only the + * verified Node 24+ baseline may claim complete response-body support. + */ +function supportsCompleteNativeResponseBodies(version: NodeVersion | null) { + return version !== null && version.major >= 24 +} + +export function hasRequiredNativeMethods(network: NativeNetworkApi | undefined) { + return REQUIRED_NATIVE_NETWORK_METHODS.every((method) => typeof network?.[method] === 'function') +} + +export function getNativeCapabilities( + version: NodeVersion | null, + network: NativeNetworkApi | undefined +): CapabilityMap { + const lifecycle = hasRequiredNativeMethods(network) + const websocketLifecycle = + lifecycle && + typeof network?.webSocketCreated === 'function' && + typeof network.webSocketHandshakeResponseReceived === 'function' && + typeof network.webSocketClosed === 'function' + + return Object.freeze({ + http: lifecycle, + https: lifecycle, + fetch: lifecycle && supportsNativeFetch(version), + http2: lifecycle && supportsNativeHttp2(version), + responseBody: + lifecycle && + supportsCompleteNativeResponseBodies(version) && + typeof network?.dataReceived === 'function', + // Deliberately not inferred from dataSent; HTTP/1 request bodies remain incomplete. + requestBody: false, + websocketLifecycle, + websocketFrames: false, + sseMessages: false, + initiator: lifecycle + }) +} + +export function getMissingCapabilities( + capabilities: CapabilityMap, + required: readonly NetworkCapability[] = [] +) { + return required.filter((capability) => capabilities[capability] !== true) +} + +export function hasNativeInspectionFlag(execArgv: readonly string[]) { + return execArgv.some( + (argument) => + argument === NATIVE_NETWORK_INSPECTION_FLAG || + argument.startsWith(`${NATIVE_NETWORK_INSPECTION_FLAG}=`) + ) +} diff --git a/packages/network-debugger/src/adapters/node-native/errors.ts b/packages/network-debugger/src/adapters/node-native/errors.ts new file mode 100644 index 0000000..5a2d7f1 --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/errors.ts @@ -0,0 +1,31 @@ +import type { Diagnostic } from '../types' + +export class NodeNativeAdapterError extends Error { + readonly code: string + readonly hint?: string + readonly diagnostics: readonly Diagnostic[] + + constructor(diagnostic: Diagnostic, diagnostics: readonly Diagnostic[] = [diagnostic]) { + super(diagnostic.message) + this.name = 'NodeNativeAdapterError' + this.code = diagnostic.code + this.hint = diagnostic.hint + this.diagnostics = diagnostics + } +} + +export function nativeDiagnostic( + code: string, + message: string, + hint?: string, + details?: Readonly>, + level: Diagnostic['level'] = 'error' +): Diagnostic { + return { + code, + level, + message, + ...(hint ? { hint } : {}), + ...(details ? { details } : {}) + } +} diff --git a/packages/network-debugger/src/adapters/node-native/index.test.ts b/packages/network-debugger/src/adapters/node-native/index.test.ts new file mode 100644 index 0000000..6d2639e --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/index.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, test, vi } from 'vitest' +import type { NativeNetworkApi } from './capability' +import { NodeNativeAdapter, NodeNativeAdapterError, type NativeInspectorApi } from './index' + +const inspectorUrl = 'ws://127.0.0.1:9229/target-id' + +function createNetwork(overrides: NativeNetworkApi = {}): NativeNetworkApi { + return { + requestWillBeSent: vi.fn(), + responseReceived: vi.fn(), + loadingFinished: vi.fn(), + loadingFailed: vi.fn(), + dataReceived: vi.fn(), + dataSent: vi.fn(), + webSocketCreated: vi.fn(), + webSocketHandshakeResponseReceived: vi.fn(), + webSocketClosed: vi.fn(), + ...overrides + } +} + +function descriptor(url = inspectorUrl) { + return [ + { + id: 'target-id', + title: 'node[123]', + type: 'node', + url: 'file:///app.js', + webSocketDebuggerUrl: url, + devtoolsFrontendUrl: 'devtools://native-target' + } + ] +} + +function createInspector(initialUrl: string | null = inspectorUrl) { + let currentUrl = initialUrl ?? undefined + const inspector: NativeInspectorApi = { + url: vi.fn(() => currentUrl), + open: vi.fn(() => { + currentUrl = inspectorUrl + }), + close: vi.fn(), + Network: createNetwork() + } + return inspector +} + +function createAdapter( + inspector: NativeInspectorApi | null, + overrides: ConstructorParameters[0] = {} +) { + return new NodeNativeAdapter({ + inspector, + inspectorAvailable: inspector !== null, + execArgv: ['--experimental-network-inspection'], + nodeVersion: '24.8.0', + requestJson: vi.fn().mockResolvedValue(descriptor()), + ...overrides + }) +} + +describe('NodeNativeAdapter probe', () => { + test('reports a supported runtime as available and Auto-selectable', () => { + const probe = createAdapter(createInspector()).probe() + expect(probe.available).toBe(true) + expect(probe.autoSelectable).toBe(true) + expect(probe.diagnostics).toEqual([]) + }) + + test('reports missing Inspector support with a stable code', () => { + const probe = createAdapter(null).probe() + expect(probe.available).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ code: 'NND_NATIVE_INSPECTOR_UNAVAILABLE' }) + ) + }) + + test('reports an unsupported runtime', () => { + const probe = createAdapter(createInspector(), { nodeVersion: '18.20.0' }).probe() + expect(probe.available).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ code: 'NND_NATIVE_RUNTIME_UNSUPPORTED' }) + ) + }) + + test('requires the experimental flag', () => { + const probe = createAdapter(createInspector(), { execArgv: [] }).probe() + expect(probe.available).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ + code: 'NND_NATIVE_FLAG_REQUIRED', + hint: expect.stringContaining('--experimental-network-inspection') + }) + ) + }) + + test('requires the native lifecycle methods', () => { + const inspector = createInspector() + inspector.Network = { requestWillBeSent: vi.fn() } + const probe = createAdapter(inspector).probe() + expect(probe.available).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ code: 'NND_NATIVE_METHODS_UNAVAILABLE' }) + ) + }) + + test('validates required capabilities', () => { + const probe = createAdapter(createInspector()).probe({ + requiredCapabilities: ['websocketFrames', 'requestBody'] + }) + expect(probe.available).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ + code: 'NND_NATIVE_REQUIRED_CAPABILITY_UNAVAILABLE', + details: { missingCapabilities: ['websocketFrames', 'requestBody'] } + }) + ) + }) + + test('keeps older supported runtimes explicit-only', () => { + const probe = createAdapter(createInspector(), { nodeVersion: '22.20.0' }).probe() + expect(probe.available).toBe(true) + expect(probe.autoSelectable).toBe(false) + expect(probe.diagnostics).toContainEqual( + expect.objectContaining({ + code: 'NND_NATIVE_AUTO_BASELINE_UNPROVEN', + level: 'warn' + }) + ) + }) +}) + +describe('NodeNativeAdapter start and ownership', () => { + test('forced start exposes the first actionable probe error', async () => { + const adapter = createAdapter(createInspector(), { execArgv: [] }) + await expect(adapter.start()).rejects.toMatchObject({ + code: 'NND_NATIVE_FLAG_REQUIRED', + hint: expect.stringContaining('--experimental-network-inspection') + }) + }) + + test('reuses an existing target without opening or closing it', async () => { + const inspector = createInspector() + const requestJson = vi.fn().mockResolvedValue(descriptor()) + const session = await createAdapter(inspector, { requestJson }).start() + + expect(inspector.open).not.toHaveBeenCalled() + expect(session.target.webSocketDebuggerUrl).toBe(inspectorUrl) + expect(requestJson).toHaveBeenCalledWith('http://127.0.0.1:9229/json/list', 500) + + await session.dispose() + expect(inspector.close).not.toHaveBeenCalled() + }) + + test('opens an OS-assigned target and disposes only its owned handle', async () => { + const inspector = createInspector(null) + const disposeSymbol = (Symbol as unknown as { dispose?: symbol }).dispose ?? Symbol('dispose') + const dispose = vi.fn() + ;(inspector.open as ReturnType).mockImplementation(() => { + ;(inspector.url as ReturnType).mockReturnValue(inspectorUrl) + return { [disposeSymbol]: dispose } + }) + + const originalSymbolDispose = (Symbol as unknown as { dispose?: symbol }).dispose + if (!originalSymbolDispose) { + Object.defineProperty(Symbol, 'dispose', { value: disposeSymbol, configurable: true }) + } + + try { + const session = await createAdapter(inspector).start({ + inspector: { host: 'localhost', port: 0 } + }) + expect(inspector.open).toHaveBeenCalledWith(0, 'localhost', false) + + await session.dispose() + await session.dispose() + expect(dispose).toHaveBeenCalledTimes(1) + expect(inspector.close).not.toHaveBeenCalled() + } finally { + if (!originalSymbolDispose) { + delete (Symbol as unknown as { dispose?: symbol }).dispose + } + } + }) + + test('closes an owned Inspector when target discovery fails', async () => { + const inspector = createInspector(null) + const adapter = createAdapter(inspector, { + requestJson: vi.fn().mockRejectedValue(new Error('connection refused')), + discovery: { attempts: 1 } + }) + + await expect(adapter.start()).rejects.toMatchObject({ + code: 'NND_NATIVE_TARGET_DISCOVERY_FAILED' + }) + expect(inspector.close).toHaveBeenCalledTimes(1) + }) + + test('does not close a reused Inspector when target discovery fails', async () => { + const inspector = createInspector() + const adapter = createAdapter(inspector, { + requestJson: vi.fn().mockRejectedValue(new Error('connection refused')), + discovery: { attempts: 1 } + }) + + await expect(adapter.start()).rejects.toMatchObject({ + code: 'NND_NATIVE_TARGET_DISCOVERY_FAILED' + }) + expect(inspector.open).not.toHaveBeenCalled() + expect(inspector.close).not.toHaveBeenCalled() + }) + + test('closes an owned Inspector that does not publish a URL', async () => { + const inspector = createInspector(null) + ;(inspector.open as ReturnType).mockImplementation(() => undefined) + + await expect(createAdapter(inspector).start()).rejects.toMatchObject({ + code: 'NND_NATIVE_TARGET_URL_UNAVAILABLE' + }) + expect(inspector.close).toHaveBeenCalledTimes(1) + }) + + test('reports Inspector open failures with a stable code', async () => { + const inspector = createInspector(null) + ;(inspector.open as ReturnType).mockImplementation(() => { + throw new Error('address in use') + }) + + await expect(createAdapter(inspector).start()).rejects.toMatchObject({ + code: 'NND_NATIVE_TARGET_OPEN_FAILED', + hint: expect.stringContaining('--inspect=0') + }) + }) +}) diff --git a/packages/network-debugger/src/adapters/node-native/index.ts b/packages/network-debugger/src/adapters/node-native/index.ts new file mode 100644 index 0000000..71b9967 --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/index.ts @@ -0,0 +1,251 @@ +import * as nodeInspector from 'node:inspector' +import type { + AdapterProbe, + AdapterSession, + AdapterStartOptions, + DebugAdapter, + Diagnostic +} from '../types' +import { + NATIVE_CAPABILITIES, + NATIVE_NETWORK_INSPECTION_FLAG, + type NativeNetworkApi, + getMissingCapabilities, + getNativeCapabilities, + hasNativeInspectionFlag, + hasRequiredNativeMethods, + isNativeAutoBaseline, + parseNodeVersion, + supportsNativeNetworkInspection +} from './capability' +import { NodeNativeAdapterError, nativeDiagnostic } from './errors' +import { + discoverInspectorTarget, + type TargetDiscoveryDependencies, + type TargetDiscoveryOptions +} from './inspector-target' + +export * from './capability' +export * from './errors' +export * from './inspector-target' + +export interface InspectorDisposable { + [key: symbol]: unknown +} + +export interface NativeInspectorApi { + url(): string | undefined + open(port?: number, host?: string, wait?: boolean): void | InspectorDisposable + close(): void + Network?: NativeNetworkApi +} + +export interface NodeNativeAdapterDependencies extends TargetDiscoveryDependencies { + inspector?: NativeInspectorApi | null + inspectorAvailable?: boolean | (() => boolean) + execArgv?: readonly string[] | (() => readonly string[]) + nodeVersion?: string | (() => string) + discovery?: TargetDiscoveryOptions +} + +function resolveValue(value: T | (() => T)): T { + return typeof value === 'function' ? (value as () => T)() : value +} + +function defaultInspectorAvailable() { + return process.features?.inspector !== false +} + +function errorDiagnostic( + code: string, + message: string, + hint: string, + details?: Readonly> +) { + return nativeDiagnostic(code, message, hint, details) +} + +export class NodeNativeAdapter implements DebugAdapter { + readonly kind = 'native' as const + private readonly dependencies: NodeNativeAdapterDependencies + + constructor(dependencies: NodeNativeAdapterDependencies = {}) { + this.dependencies = dependencies + } + + probe(options: AdapterStartOptions = {}): AdapterProbe { + const diagnostics: Diagnostic[] = [] + const inspector = + this.dependencies.inspector === undefined + ? (nodeInspector as unknown as NativeInspectorApi) + : this.dependencies.inspector + const inspectorAvailable = resolveValue( + this.dependencies.inspectorAvailable ?? defaultInspectorAvailable + ) + const nodeVersionText = resolveValue( + this.dependencies.nodeVersion ?? (() => process.versions.node) + ) + const version = parseNodeVersion(nodeVersionText) + const execArgv = resolveValue(this.dependencies.execArgv ?? (() => process.execArgv)) + const network = inspector?.Network + const capabilities = getNativeCapabilities(version, network) + + if (!inspectorAvailable || !inspector) { + diagnostics.push( + errorDiagnostic( + 'NND_NATIVE_INSPECTOR_UNAVAILABLE', + 'This Node.js runtime was built without Inspector support.', + 'Use an official Node.js build with Inspector support, or select the Legacy adapter.' + ) + ) + } + + if (!supportsNativeNetworkInspection(version)) { + diagnostics.push( + errorDiagnostic( + 'NND_NATIVE_RUNTIME_UNSUPPORTED', + `Node.js ${nodeVersionText} does not support native network inspection.`, + 'Upgrade to Node.js 20.18+, 22.6+, or a newer release, or select the Legacy adapter.', + { nodeVersion: nodeVersionText } + ) + ) + } + + if (!hasNativeInspectionFlag(execArgv)) { + diagnostics.push( + errorDiagnostic( + 'NND_NATIVE_FLAG_REQUIRED', + `Native network inspection requires ${NATIVE_NETWORK_INSPECTION_FLAG}.`, + `Restart with: node --inspect=0 ${NATIVE_NETWORK_INSPECTION_FLAG} `, + { execArgv: [...execArgv] } + ) + ) + } + + if (inspector && !hasRequiredNativeMethods(network)) { + diagnostics.push( + errorDiagnostic( + 'NND_NATIVE_METHODS_UNAVAILABLE', + 'The required node:inspector Network methods are unavailable in this runtime.', + 'Upgrade Node.js or select the Legacy adapter.' + ) + ) + } + + const missingCapabilities = getMissingCapabilities(capabilities, options.requiredCapabilities) + if (missingCapabilities.length > 0) { + diagnostics.push( + errorDiagnostic( + 'NND_NATIVE_REQUIRED_CAPABILITY_UNAVAILABLE', + `Native network inspection cannot provide: ${missingCapabilities.join(', ')}.`, + 'Remove the unsupported requirement or select an adapter that provides it.', + { missingCapabilities } + ) + ) + } + + const runtimeSupported = supportsNativeNetworkInspection(version) + if (runtimeSupported && !isNativeAutoBaseline(version)) { + diagnostics.push( + nativeDiagnostic( + 'NND_NATIVE_AUTO_BASELINE_UNPROVEN', + `Node.js ${nodeVersionText} supports explicit Native mode but is below the proven Auto baseline.`, + 'Use Node.js 24.7+ for Auto selection, or explicitly select Native mode.', + { nodeVersion: nodeVersionText }, + 'warn' + ) + ) + } + + const available = !diagnostics.some((diagnostic) => diagnostic.level === 'error') + return { + kind: this.kind, + available, + autoSelectable: available && isNativeAutoBaseline(version), + capabilities, + diagnostics + } + } + + async start(options: AdapterStartOptions = {}): Promise { + const probe = this.probe(options) + if (!probe.available) { + const diagnostic = probe.diagnostics.find((item) => item.level === 'error')! + throw new NodeNativeAdapterError(diagnostic, probe.diagnostics) + } + + const inspector = ( + this.dependencies.inspector === undefined + ? (nodeInspector as unknown as NativeInspectorApi) + : this.dependencies.inspector + )! + let inspectorUrl = inspector.url() + let owned = false + let disposable: void | InspectorDisposable = undefined + + if (!inspectorUrl) { + const port = options.inspector?.port ?? 0 + const host = options.inspector?.host ?? '127.0.0.1' + try { + disposable = inspector.open(port, host, false) + owned = true + inspectorUrl = inspector.url() + } catch (error) { + const diagnostic = errorDiagnostic( + 'NND_NATIVE_TARGET_OPEN_FAILED', + `Unable to open the Node Inspector on ${host}:${port}.`, + 'Choose another Inspector port or start Node with --inspect=0.', + { cause: error instanceof Error ? error.message : String(error), host, port } + ) + throw new NodeNativeAdapterError(diagnostic, [...probe.diagnostics, diagnostic]) + } + + if (!inspectorUrl) { + this.closeOwnedInspector(inspector, disposable) + const diagnostic = errorDiagnostic( + 'NND_NATIVE_TARGET_URL_UNAVAILABLE', + 'Node Inspector opened without publishing a target URL.', + 'Start Node with --inspect=0 and retry.' + ) + throw new NodeNativeAdapterError(diagnostic, [...probe.diagnostics, diagnostic]) + } + } + + try { + const target = await discoverInspectorTarget( + inspectorUrl, + this.dependencies.discovery, + this.dependencies + ) + let disposed = false + + return { + kind: this.kind, + capabilities: probe.capabilities, + target, + diagnostics: probe.diagnostics, + dispose: async () => { + if (disposed) return + disposed = true + if (owned) this.closeOwnedInspector(inspector, disposable) + } + } + } catch (error) { + if (owned) this.closeOwnedInspector(inspector, disposable) + throw error + } + } + + private closeOwnedInspector( + inspector: NativeInspectorApi, + disposable: void | InspectorDisposable + ) { + const disposeSymbol = (Symbol as unknown as { dispose?: symbol }).dispose + const dispose = disposeSymbol && disposable?.[disposeSymbol] + if (typeof dispose === 'function') { + dispose.call(disposable) + return + } + inspector.close() + } +} diff --git a/packages/network-debugger/src/adapters/node-native/inspector-target.test.ts b/packages/network-debugger/src/adapters/node-native/inspector-target.test.ts new file mode 100644 index 0000000..084dae6 --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/inspector-target.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test, vi } from 'vitest' +import { NodeNativeAdapterError } from './errors' +import { discoverInspectorTarget, getInspectorDiscoveryUrl } from './inspector-target' + +describe('Inspector target discovery', () => { + test('maps an Inspector WebSocket URL to the standard discovery endpoint', () => { + expect(getInspectorDiscoveryUrl('ws://127.0.0.1:9229/target-id')).toBe( + 'http://127.0.0.1:9229/json/list' + ) + }) + + test('rejects non-Inspector protocols', () => { + expect(() => getInspectorDiscoveryUrl('http://127.0.0.1:9229/target-id')).toThrow( + 'Unsupported Inspector URL protocol' + ) + expect(() => getInspectorDiscoveryUrl('wss://localhost:9443/target-id')).toThrow( + 'Unsupported Inspector URL protocol' + ) + }) + + test('selects the descriptor matching the Inspector target id', async () => { + const requestJson = vi.fn().mockResolvedValue([ + { + id: 'different', + title: 'different', + type: 'node', + url: 'file://', + webSocketDebuggerUrl: 'ws://127.0.0.1:9229/different' + }, + { + id: 'target-id', + title: 'node[123]', + type: 'node', + url: 'file:///app.js', + webSocketDebuggerUrl: 'ws://localhost:9229/target-id', + devtoolsFrontendUrl: 'devtools://native-target' + } + ]) + + const target = await discoverInspectorTarget( + 'ws://127.0.0.1:9229/target-id', + { attempts: 1 }, + { requestJson } + ) + + expect(target).toEqual({ + id: 'target-id', + title: 'node[123]', + type: 'node', + url: 'file:///app.js', + webSocketDebuggerUrl: 'ws://localhost:9229/target-id', + devtoolsFrontendUrl: 'devtools://native-target', + discoveryUrl: 'http://127.0.0.1:9229/json/list' + }) + }) + + test('retries discovery with bounded attempts', async () => { + const requestJson = vi + .fn() + .mockRejectedValueOnce(new Error('not listening')) + .mockResolvedValueOnce([ + { + id: 'target-id', + webSocketDebuggerUrl: 'ws://127.0.0.1:9229/target-id' + } + ]) + const sleep = vi.fn().mockResolvedValue(undefined) + + const target = await discoverInspectorTarget( + 'ws://127.0.0.1:9229/target-id', + { attempts: 2, requestTimeoutMs: 10, retryDelayMs: 1 }, + { requestJson, sleep } + ) + + expect(target.id).toBe('target-id') + expect(requestJson).toHaveBeenCalledTimes(2) + expect(requestJson).toHaveBeenCalledWith('http://127.0.0.1:9229/json/list', 10) + expect(sleep).toHaveBeenCalledWith(1) + }) + + test('fails with a stable discovery code after exhausting retries', async () => { + const requestJson = vi.fn().mockRejectedValue(new Error('connection refused')) + + await expect( + discoverInspectorTarget( + 'ws://127.0.0.1:9229/target-id', + { attempts: 2, retryDelayMs: 0 }, + { requestJson, sleep: vi.fn().mockResolvedValue(undefined) } + ) + ).rejects.toMatchObject({ + code: 'NND_NATIVE_TARGET_DISCOVERY_FAILED' + }) + expect(requestJson).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/network-debugger/src/adapters/node-native/inspector-target.ts b/packages/network-debugger/src/adapters/node-native/inspector-target.ts new file mode 100644 index 0000000..cea700d --- /dev/null +++ b/packages/network-debugger/src/adapters/node-native/inspector-target.ts @@ -0,0 +1,178 @@ +import * as http from 'node:http' +import type { DevtoolsTarget } from '../types' +import { NodeNativeAdapterError, nativeDiagnostic } from './errors' + +export interface RawInspectorTarget { + id?: unknown + title?: unknown + type?: unknown + url?: unknown + webSocketDebuggerUrl?: unknown + devtoolsFrontendUrl?: unknown + devtoolsFrontendUrlCompat?: unknown +} + +export interface TargetDiscoveryOptions { + attempts?: number + requestTimeoutMs?: number + retryDelayMs?: number +} + +export interface TargetDiscoveryDependencies { + requestJson?: (url: string, timeoutMs: number) => Promise + sleep?: (milliseconds: number) => Promise +} + +const DEFAULT_DISCOVERY_OPTIONS: Required = { + attempts: 5, + requestTimeoutMs: 500, + retryDelayMs: 25 +} + +function requestJson(url: string, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const parsed = new URL(url) + // Discovery targets an Inspector server that this process may own. A + // pooled keep-alive socket can outlive the response and make the + // synchronous inspector.close() wait on its own client connection, + // particularly on Windows. Give each probe an isolated, non-reusing + // agent so the response fully releases the target before disposal. + const request = http.get(parsed, { agent: false }, (response) => { + const chunks: Buffer[] = [] + const socket = response.socket + + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.on('end', () => { + const statusCode = response.statusCode ?? 0 + let result: unknown + let failure: unknown + if (statusCode < 200 || statusCode >= 300) { + failure = new Error(`Inspector discovery returned HTTP ${statusCode}`) + } else { + try { + result = JSON.parse(Buffer.concat(chunks).toString('utf8')) + } catch (error) { + failure = error + } + } + + const settle = () => (failure ? reject(failure) : resolve(result)) + // `end` means the payload is complete, not that the underlying socket + // has left the Inspector server. Wait for actual close before allowing + // an owning adapter to call the synchronous inspector.close(). + if (socket.destroyed) { + settle() + } else { + socket.once('close', settle) + socket.destroy() + } + }) + }) + + request.setTimeout(timeoutMs, () => { + request.destroy(new Error(`Inspector discovery timed out after ${timeoutMs}ms`)) + }) + request.on('error', reject) + }) +} + +function sleep(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + +export function getInspectorDiscoveryUrl(webSocketUrl: string) { + const parsed = new URL(webSocketUrl) + // node:inspector exposes a local, non-TLS WebSocket endpoint. Supporting + // arbitrary remote WSS proxies here would make target ownership ambiguous. + if (parsed.protocol !== 'ws:') { + throw new Error(`Unsupported Inspector URL protocol: ${parsed.protocol}`) + } + parsed.protocol = 'http:' + parsed.pathname = '/json/list' + parsed.search = '' + parsed.hash = '' + return parsed.toString() +} + +function isString(value: unknown): value is string { + return typeof value === 'string' +} + +function normalizeTarget( + rawTarget: RawInspectorTarget, + discoveryUrl: string +): DevtoolsTarget | null { + if (!isString(rawTarget.id) || !isString(rawTarget.webSocketDebuggerUrl)) return null + + return { + id: rawTarget.id, + title: isString(rawTarget.title) ? rawTarget.title : 'Node.js', + type: isString(rawTarget.type) ? rawTarget.type : 'node', + url: isString(rawTarget.url) ? rawTarget.url : '', + webSocketDebuggerUrl: rawTarget.webSocketDebuggerUrl, + ...(isString(rawTarget.devtoolsFrontendUrl) + ? { devtoolsFrontendUrl: rawTarget.devtoolsFrontendUrl } + : {}), + ...(isString(rawTarget.devtoolsFrontendUrlCompat) + ? { devtoolsFrontendUrlCompat: rawTarget.devtoolsFrontendUrlCompat } + : {}), + discoveryUrl + } +} + +function selectTarget(payload: unknown, inspectorUrl: string, discoveryUrl: string) { + if (!Array.isArray(payload)) return null + + const targets = payload + .map((target) => normalizeTarget(target as RawInspectorTarget, discoveryUrl)) + .filter((target): target is DevtoolsTarget => target !== null) + + const inspectorId = new URL(inspectorUrl).pathname.replace(/^\//, '') + return ( + targets.find((target) => target.webSocketDebuggerUrl === inspectorUrl) ?? + targets.find((target) => target.id === inspectorId) ?? + targets.find((target) => target.type === 'node') ?? + targets[0] ?? + null + ) +} + +export async function discoverInspectorTarget( + inspectorUrl: string, + options: TargetDiscoveryOptions = {}, + dependencies: TargetDiscoveryDependencies = {} +): Promise { + const normalizedOptions = { ...DEFAULT_DISCOVERY_OPTIONS, ...options } + const fetchJson = dependencies.requestJson ?? requestJson + const wait = dependencies.sleep ?? sleep + const discoveryUrl = getInspectorDiscoveryUrl(inspectorUrl) + let lastError: unknown + + for (let attempt = 1; attempt <= normalizedOptions.attempts; attempt++) { + try { + const payload = await fetchJson(discoveryUrl, normalizedOptions.requestTimeoutMs) + const target = selectTarget(payload, inspectorUrl, discoveryUrl) + if (target) return target + throw new Error('Inspector discovery did not return a matching Node target') + } catch (error) { + lastError = error + if (attempt < normalizedOptions.attempts) { + await wait(normalizedOptions.retryDelayMs) + } + } + } + + const diagnostic = nativeDiagnostic( + 'NND_NATIVE_TARGET_DISCOVERY_FAILED', + `Unable to discover the Node Inspector target at ${discoveryUrl}.`, + 'Verify that the Inspector endpoint is still running and retry.', + { + inspectorUrl, + discoveryUrl, + attempts: normalizedOptions.attempts, + cause: lastError instanceof Error ? lastError.message : String(lastError) + } + ) + throw new NodeNativeAdapterError(diagnostic) +} diff --git a/packages/network-debugger/src/adapters/selector.test.ts b/packages/network-debugger/src/adapters/selector.test.ts new file mode 100644 index 0000000..9bf4439 --- /dev/null +++ b/packages/network-debugger/src/adapters/selector.test.ts @@ -0,0 +1,479 @@ +import { describe, expect, test, vi } from 'vitest' +import { AdapterSelectionError, AdapterSelector, type AdapterSelectionErrorCode } from './selector' +import { + NETWORK_CAPABILITIES, + type AdapterKind, + type AdapterProbe, + type CapabilityMap, + type DebugAdapter, + type Diagnostic, + type NetworkCapability +} from './types' + +const capabilities = ( + enabled: readonly NetworkCapability[] = NETWORK_CAPABILITIES +): CapabilityMap => { + const enabledSet = new Set(enabled) + return Object.fromEntries( + NETWORK_CAPABILITIES.map((capability) => [capability, enabledSet.has(capability)]) + ) as unknown as CapabilityMap +} + +const diagnostic = (code: string): Diagnostic => ({ + code, + level: 'warn', + message: code +}) + +const adapter = ( + kind: AdapterKind, + probe: AdapterProbe | (() => AdapterProbe | Promise) +): DebugAdapter => ({ + kind, + probe: vi.fn(typeof probe === 'function' ? probe : () => probe), + start: vi.fn(async () => ({ + kind, + capabilities: capabilities(), + target: { + id: kind, + title: kind, + type: 'node', + url: 'file://', + webSocketDebuggerUrl: `ws://127.0.0.1/${kind}`, + discoveryUrl: 'http://127.0.0.1/json/list' + }, + diagnostics: [], + dispose: async () => {} + })) as DebugAdapter['start'] +}) + +const probe = ( + kind: AdapterKind, + options: { + available?: boolean + enabled?: readonly NetworkCapability[] + diagnostics?: readonly Diagnostic[] + autoSelectable?: boolean + } = {} +): AdapterProbe => ({ + kind, + available: options.available ?? true, + ...(options.autoSelectable === undefined ? {} : { autoSelectable: options.autoSelectable }), + capabilities: capabilities(options.enabled), + diagnostics: options.diagnostics ?? [] +}) + +const expectSelectionError = async ( + promise: Promise, + code: AdapterSelectionErrorCode +): Promise => { + try { + await promise + } catch (error) { + expect(error).toBeInstanceOf(AdapterSelectionError) + expect((error as AdapterSelectionError).code).toBe(code) + return error as AdapterSelectionError + } + throw new Error(`Expected ${code}`) +} + +describe('AdapterSelector', () => { + test('auto prefers a capable native adapter and does not probe legacy', async () => { + const native = adapter('native', probe('native')) + const legacy = adapter('legacy', probe('legacy')) + + const selection = await new AdapterSelector([legacy, native]).select({ + mode: 'auto', + requiredCapabilities: ['fetch', 'responseBody'] + }) + + expect(selection).toEqual({ + adapter: native, + probe: probe('native') + }) + expect(native.probe).toHaveBeenCalledOnce() + expect(native.probe).toHaveBeenCalledWith({ + requiredCapabilities: ['fetch', 'responseBody'] + }) + expect(legacy.probe).not.toHaveBeenCalled() + }) + + test('auto is the default mode and supports a synchronous probe', async () => { + const nativeProbe = probe('native') + const native = adapter('native', () => nativeProbe) + + const selection = await new AdapterSelector([native]).select() + + expect(selection.adapter).toBe(native) + expect(selection.probe).toBe(nativeProbe) + expect(selection.fallbackReason).toBeUndefined() + }) + + test('supports an asynchronous probe and forwards inspector options', async () => { + const nativeProbe = probe('native') + const native = adapter('native', async () => nativeProbe) + + const selection = await new AdapterSelector([native]).select({ + mode: 'native', + inspector: { host: '127.0.0.1', port: 9230 } + }) + + expect(selection.probe).toBe(nativeProbe) + expect(native.probe).toHaveBeenCalledWith({ + inspector: { host: '127.0.0.1', port: 9230 } + }) + }) + + test('normalizes duplicate and unordered required capabilities deterministically', async () => { + const native = adapter('native', probe('native')) + + await new AdapterSelector([native]).select({ + mode: 'native', + requiredCapabilities: ['responseBody', 'http', 'responseBody'] + }) + + expect(native.probe).toHaveBeenCalledWith({ + requiredCapabilities: ['http', 'responseBody'] + }) + }) + + test('forced native fails with a stable unavailable error when not registered', async () => { + const error = await expectSelectionError( + new AdapterSelector([]).select({ mode: 'native' }), + 'NND_ADAPTER_UNAVAILABLE' + ) + + expect(error.message).toBe('Adapter "native" is not registered.') + expect(error.details).toEqual({ + kind: 'native', + requiredCapabilities: [], + reason: 'not_registered' + }) + }) + + test('forced native fails with a stable unavailable error and retains diagnostics', async () => { + const native = adapter( + 'native', + probe('native', { + available: false, + diagnostics: [diagnostic('NND_NATIVE_FLAG_MISSING')] + }) + ) + + const error = await expectSelectionError( + new AdapterSelector([native]).select({ mode: 'native' }), + 'NND_ADAPTER_UNAVAILABLE' + ) + + expect(error.message).toBe('Adapter "native" is unavailable.') + expect(error.details).toEqual({ + kind: 'native', + requiredCapabilities: [], + reason: 'unavailable', + diagnosticCodes: ['NND_NATIVE_FLAG_MISSING'], + diagnostics: [diagnostic('NND_NATIVE_FLAG_MISSING')] + }) + }) + + test('forced legacy fails with missing capabilities in canonical order', async () => { + const legacy = adapter( + 'legacy', + probe('legacy', { + enabled: ['https'] + }) + ) + + const error = await expectSelectionError( + new AdapterSelector([legacy]).select({ + mode: 'legacy', + requiredCapabilities: ['responseBody', 'http', 'fetch'] + }), + 'NND_ADAPTER_CAPABILITY_MISSING' + ) + + expect(error.message).toBe( + 'Adapter "legacy" does not provide required capabilities: http, fetch, responseBody.' + ) + expect(error.details).toEqual({ + kind: 'legacy', + requiredCapabilities: ['http', 'fetch', 'responseBody'], + reason: 'missing_capabilities', + missingCapabilities: ['http', 'fetch', 'responseBody'], + diagnosticCodes: [] + }) + }) + + test('forced mode wraps a rejected probe in a stable error', async () => { + const native = adapter('native', async () => { + throw new Error('probe exploded') + }) + + const error = await expectSelectionError( + new AdapterSelector([native]).select({ mode: 'native' }), + 'NND_ADAPTER_PROBE_FAILED' + ) + + expect(error.message).toBe('Adapter "native" probe failed: probe exploded.') + expect(error.details).toEqual({ + kind: 'native', + requiredCapabilities: [], + reason: 'probe_failed', + error: 'probe exploded' + }) + }) + + test('forced mode rejects a probe whose kind does not match its adapter', async () => { + const native = adapter('native', probe('legacy')) + + const error = await expectSelectionError( + new AdapterSelector([native]).select({ mode: 'native' }), + 'NND_INVALID_ADAPTER_PROBE' + ) + + expect(error.details).toEqual({ + kind: 'native', + requiredCapabilities: [], + reason: 'invalid_probe', + error: 'Expected "native", received "legacy".' + }) + }) + + test('auto falls back when native is unavailable with a deterministic diagnostic', async () => { + const native = adapter( + 'native', + probe('native', { + available: false, + diagnostics: [diagnostic('NND_NATIVE_FLAG_MISSING')] + }) + ) + const legacyProbe = probe('legacy') + const legacy = adapter('legacy', legacyProbe) + + const selection = await new AdapterSelector([native, legacy]).select({ + requiredCapabilities: ['fetch'] + }) + + expect(selection.adapter).toBe(legacy) + expect(selection.probe).toBe(legacyProbe) + expect(selection.fallbackReason).toEqual({ + code: 'NND_AUTO_FALLBACK', + level: 'warn', + message: 'Native adapter cannot satisfy this selection; using legacy adapter.', + hint: 'Use mode "native" to fail instead of falling back.', + details: { + from: 'native', + to: 'legacy', + reason: 'unavailable', + requiredCapabilities: ['fetch'], + diagnosticCodes: ['NND_NATIVE_FLAG_MISSING'], + diagnostics: [diagnostic('NND_NATIVE_FLAG_MISSING')] + } + }) + }) + + test('auto falls back when native is available but not auto-selectable', async () => { + const baselineDiagnostic = diagnostic('NND_NATIVE_AUTO_BASELINE_UNMET') + const nativeProbe = probe('native', { + autoSelectable: false, + diagnostics: [baselineDiagnostic] + }) + const native = adapter('native', nativeProbe) + const legacy = adapter('legacy', probe('legacy')) + + const selection = await new AdapterSelector([native, legacy]).select({ + requiredCapabilities: ['http', 'fetch'] + }) + + expect(selection.adapter).toBe(legacy) + expect(selection.fallbackReason?.details).toEqual({ + from: 'native', + to: 'legacy', + reason: 'not_auto_selectable', + requiredCapabilities: ['http', 'fetch'], + diagnosticCodes: ['NND_NATIVE_AUTO_BASELINE_UNMET'], + diagnostics: [baselineDiagnostic] + }) + }) + + test('forced native ignores autoSelectable while still enforcing capabilities', async () => { + const nativeProbe = probe('native', { + autoSelectable: false, + enabled: ['http'], + diagnostics: [diagnostic('NND_NATIVE_AUTO_BASELINE_UNMET')] + }) + const native = adapter('native', nativeProbe) + + const selection = await new AdapterSelector([native]).select({ + mode: 'native', + requiredCapabilities: ['http'] + }) + + expect(selection).toEqual({ adapter: native, probe: nativeProbe }) + }) + + test('auto falls back when native lacks requirements and explains which ones', async () => { + const native = adapter( + 'native', + probe('native', { + enabled: ['http'] + }) + ) + const legacy = adapter('legacy', probe('legacy')) + + const selection = await new AdapterSelector([native, legacy]).select({ + requiredCapabilities: ['responseBody', 'http', 'fetch'] + }) + + expect(selection.adapter).toBe(legacy) + expect(selection.fallbackReason?.details).toEqual({ + from: 'native', + to: 'legacy', + reason: 'missing_capabilities', + requiredCapabilities: ['http', 'fetch', 'responseBody'], + missingCapabilities: ['fetch', 'responseBody'], + diagnosticCodes: [] + }) + }) + + test('auto falls back when native is not registered', async () => { + const legacy = adapter('legacy', probe('legacy')) + + const selection = await new AdapterSelector([legacy]).select() + + expect(selection.adapter).toBe(legacy) + expect(selection.fallbackReason?.details).toEqual({ + from: 'native', + to: 'legacy', + reason: 'not_registered', + requiredCapabilities: [] + }) + }) + + test('auto falls back after a native probe failure', async () => { + const native = adapter('native', () => { + throw 'synchronous failure' + }) + const legacy = adapter('legacy', probe('legacy')) + + const selection = await new AdapterSelector([native, legacy]).select() + + expect(selection.adapter).toBe(legacy) + expect(selection.fallbackReason?.details).toEqual({ + from: 'native', + to: 'legacy', + reason: 'probe_failed', + requiredCapabilities: [], + error: 'synchronous failure' + }) + }) + + test('auto fails explicitly with deterministic attempts when neither adapter is capable', async () => { + const native = adapter( + 'native', + probe('native', { + available: false, + diagnostics: [diagnostic('NND_NATIVE_UNAVAILABLE')] + }) + ) + const legacy = adapter( + 'legacy', + probe('legacy', { + enabled: ['http'] + }) + ) + + const error = await expectSelectionError( + new AdapterSelector([legacy, native]).select({ + requiredCapabilities: ['fetch', 'responseBody'] + }), + 'NND_NO_CAPABLE_ADAPTER' + ) + + expect(error.message).toBe( + 'No available adapter satisfies required capabilities: fetch, responseBody.' + ) + expect(error.details).toEqual({ + requiredCapabilities: ['fetch', 'responseBody'], + attempts: [ + { + kind: 'native', + reason: 'unavailable', + diagnosticCodes: ['NND_NATIVE_UNAVAILABLE'], + diagnostics: [diagnostic('NND_NATIVE_UNAVAILABLE')] + }, + { + kind: 'legacy', + reason: 'missing_capabilities', + missingCapabilities: ['fetch', 'responseBody'], + diagnosticCodes: [] + } + ] + }) + }) + + test('auto does not select a legacy adapter that explicitly opts out', async () => { + const legacyDiagnostic = diagnostic('NND_LEGACY_AUTO_DISABLED') + const legacy = adapter( + 'legacy', + probe('legacy', { + autoSelectable: false, + diagnostics: [legacyDiagnostic] + }) + ) + + const error = await expectSelectionError( + new AdapterSelector([legacy]).select(), + 'NND_NO_CAPABLE_ADAPTER' + ) + + expect(error.details).toEqual({ + requiredCapabilities: [], + attempts: [ + { kind: 'native', reason: 'not_registered' }, + { + kind: 'legacy', + reason: 'not_auto_selectable', + diagnosticCodes: ['NND_LEGACY_AUTO_DISABLED'], + diagnostics: [legacyDiagnostic] + } + ] + }) + }) + + test('auto reports no available adapter when none are registered', async () => { + const error = await expectSelectionError( + new AdapterSelector([]).select(), + 'NND_NO_CAPABLE_ADAPTER' + ) + + expect(error.message).toBe('No available adapter satisfies required capabilities: (none).') + expect(error.details).toEqual({ + requiredCapabilities: [], + attempts: [ + { kind: 'native', reason: 'not_registered' }, + { kind: 'legacy', reason: 'not_registered' } + ] + }) + }) + + test('constructor rejects duplicate adapter kinds deterministically', () => { + const first = adapter('native', probe('native')) + const second = adapter('native', probe('native')) + + expect(() => new AdapterSelector([first, second])).toThrowError( + expect.objectContaining({ + code: 'NND_DUPLICATE_ADAPTER', + message: 'Adapter "native" is registered more than once.', + details: { kind: 'native' } + }) + ) + }) + + test('selection never starts an adapter', async () => { + const native = adapter('native', probe('native')) + + await new AdapterSelector([native]).select({ mode: 'native' }) + + expect(native.start).not.toHaveBeenCalled() + }) +}) diff --git a/packages/network-debugger/src/adapters/selector.ts b/packages/network-debugger/src/adapters/selector.ts new file mode 100644 index 0000000..557dd87 --- /dev/null +++ b/packages/network-debugger/src/adapters/selector.ts @@ -0,0 +1,350 @@ +import { + NETWORK_CAPABILITIES, + type AdapterKind, + type AdapterMode, + type AdapterProbe, + type AdapterSelection, + type AdapterStartOptions, + type DebugAdapter, + type Diagnostic, + type NetworkCapability +} from './types' + +export interface AdapterSelectionOptions extends AdapterStartOptions { + /** + * `auto` prefers the native adapter and falls back to legacy only when the + * native adapter is unavailable or cannot provide every required capability. + * + * @default 'auto' + */ + mode?: AdapterMode +} + +export type AdapterSelectionErrorCode = + | 'NND_DUPLICATE_ADAPTER' + | 'NND_ADAPTER_UNAVAILABLE' + | 'NND_ADAPTER_PROBE_FAILED' + | 'NND_ADAPTER_CAPABILITY_MISSING' + | 'NND_INVALID_ADAPTER_PROBE' + | 'NND_NO_CAPABLE_ADAPTER' + +export type AdapterSelectionFailureReason = + | 'not_registered' + | 'probe_failed' + | 'unavailable' + | 'not_auto_selectable' + | 'missing_capabilities' + | 'invalid_probe' + +export interface AdapterSelectionAttempt { + kind: AdapterKind + reason: AdapterSelectionFailureReason + missingCapabilities?: readonly NetworkCapability[] + diagnosticCodes?: readonly string[] + diagnostics?: readonly Diagnostic[] + error?: string +} + +export class AdapterSelectionError extends Error { + readonly code: AdapterSelectionErrorCode + readonly details: Readonly> + readonly cause?: unknown + + constructor( + code: AdapterSelectionErrorCode, + message: string, + details: Readonly> = {}, + cause?: unknown + ) { + super(message) + this.name = 'AdapterSelectionError' + this.code = code + this.details = details + this.cause = cause + } +} + +interface SuccessfulProbe { + adapter: DebugAdapter + probe: AdapterProbe +} + +interface FailedProbe { + adapter?: DebugAdapter + attempt: AdapterSelectionAttempt +} + +type ProbeResult = SuccessfulProbe | FailedProbe + +const isSuccessfulProbe = (result: ProbeResult): result is SuccessfulProbe => 'probe' in result + +const normalizeRequiredCapabilities = ( + capabilities: readonly NetworkCapability[] | undefined +): readonly NetworkCapability[] => { + if (!capabilities?.length) return [] + + const requested = new Set(capabilities) + return NETWORK_CAPABILITIES.filter((capability) => requested.has(capability)) +} + +const getMissingCapabilities = ( + probe: AdapterProbe, + requiredCapabilities: readonly NetworkCapability[] +): readonly NetworkCapability[] => + requiredCapabilities.filter((capability) => !probe.capabilities[capability]) + +const errorMessage = (error: unknown): string => { + if (error instanceof Error) return error.message + return String(error) +} + +const diagnosticCodes = (probe: AdapterProbe): readonly string[] => + probe.diagnostics.map((diagnostic) => diagnostic.code) + +/** + * Selects a runtime adapter without starting it. + * + * Selection is deliberately deterministic: adapters are addressed by kind, + * `auto` always probes native first, and capability lists use the canonical + * NETWORK_CAPABILITIES order. + */ +export class AdapterSelector { + private readonly adapters: ReadonlyMap + + constructor(adapters: readonly DebugAdapter[]) { + const byKind = new Map() + + for (const adapter of adapters) { + if (byKind.has(adapter.kind)) { + throw new AdapterSelectionError( + 'NND_DUPLICATE_ADAPTER', + `Adapter "${adapter.kind}" is registered more than once.`, + { kind: adapter.kind } + ) + } + byKind.set(adapter.kind, adapter) + } + + this.adapters = byKind + } + + async select(options: AdapterSelectionOptions = {}): Promise { + const mode = options.mode ?? 'auto' + const requiredCapabilities = normalizeRequiredCapabilities(options.requiredCapabilities) + const probeOptions: AdapterStartOptions = { + ...(options.inspector ? { inspector: options.inspector } : {}), + ...(requiredCapabilities.length > 0 ? { requiredCapabilities } : {}) + } + + if (mode === 'native' || mode === 'legacy') { + return this.selectForced(mode, requiredCapabilities, probeOptions) + } + + return this.selectAuto(requiredCapabilities, probeOptions) + } + + private async selectForced( + kind: AdapterKind, + requiredCapabilities: readonly NetworkCapability[], + probeOptions: AdapterStartOptions + ): Promise { + const result = await this.probe(kind, requiredCapabilities, probeOptions) + + if (isSuccessfulProbe(result)) { + return { + adapter: result.adapter, + probe: result.probe + } + } + + const { attempt } = result + const details = { + ...attempt, + requiredCapabilities + } + + switch (attempt.reason) { + case 'not_registered': + throw new AdapterSelectionError( + 'NND_ADAPTER_UNAVAILABLE', + `Adapter "${kind}" is not registered.`, + details + ) + case 'unavailable': + throw new AdapterSelectionError( + 'NND_ADAPTER_UNAVAILABLE', + `Adapter "${kind}" is unavailable.`, + details + ) + case 'missing_capabilities': + throw new AdapterSelectionError( + 'NND_ADAPTER_CAPABILITY_MISSING', + `Adapter "${kind}" does not provide required capabilities: ${attempt.missingCapabilities!.join(', ')}.`, + details + ) + case 'not_auto_selectable': + // This result is only produced by auto-mode probes. + throw new AdapterSelectionError( + 'NND_ADAPTER_UNAVAILABLE', + `Adapter "${kind}" is not eligible for automatic selection.`, + details + ) + case 'invalid_probe': + throw new AdapterSelectionError( + 'NND_INVALID_ADAPTER_PROBE', + `Adapter "${kind}" returned a probe for a different adapter kind.`, + details + ) + case 'probe_failed': + throw new AdapterSelectionError( + 'NND_ADAPTER_PROBE_FAILED', + `Adapter "${kind}" probe failed: ${attempt.error}.`, + details, + attempt.error + ) + } + } + + private async selectAuto( + requiredCapabilities: readonly NetworkCapability[], + probeOptions: AdapterStartOptions + ): Promise { + const nativeResult = await this.probe('native', requiredCapabilities, probeOptions, true) + + if (isSuccessfulProbe(nativeResult)) { + return { + adapter: nativeResult.adapter, + probe: nativeResult.probe + } + } + + const legacyResult = await this.probe('legacy', requiredCapabilities, probeOptions, true) + + if (isSuccessfulProbe(legacyResult)) { + return { + adapter: legacyResult.adapter, + probe: legacyResult.probe, + fallbackReason: this.createFallbackDiagnostic(nativeResult.attempt, requiredCapabilities) + } + } + + const attempts = [nativeResult.attempt, legacyResult.attempt] + const requirementText = requiredCapabilities.length ? requiredCapabilities.join(', ') : '(none)' + + throw new AdapterSelectionError( + 'NND_NO_CAPABLE_ADAPTER', + `No available adapter satisfies required capabilities: ${requirementText}.`, + { + requiredCapabilities, + attempts + } + ) + } + + private async probe( + kind: AdapterKind, + requiredCapabilities: readonly NetworkCapability[], + options: AdapterStartOptions, + requireAutoSelectable = false + ): Promise { + const adapter = this.adapters.get(kind) + + if (!adapter) { + return { + attempt: { + kind, + reason: 'not_registered' + } + } + } + + let probe: AdapterProbe + try { + probe = await adapter.probe(options) + } catch (error) { + return { + adapter, + attempt: { + kind, + reason: 'probe_failed', + error: errorMessage(error) + } + } + } + + if (probe.kind !== kind) { + return { + adapter, + attempt: { + kind, + reason: 'invalid_probe', + error: `Expected "${kind}", received "${probe.kind}".` + } + } + } + + if (!probe.available) { + return { + adapter, + attempt: { + kind, + reason: 'unavailable', + diagnosticCodes: diagnosticCodes(probe), + ...(probe.diagnostics.length ? { diagnostics: probe.diagnostics } : {}) + } + } + } + + if (requireAutoSelectable && probe.autoSelectable === false) { + return { + adapter, + attempt: { + kind, + reason: 'not_auto_selectable', + diagnosticCodes: diagnosticCodes(probe), + ...(probe.diagnostics.length ? { diagnostics: probe.diagnostics } : {}) + } + } + } + + const missingCapabilities = getMissingCapabilities(probe, requiredCapabilities) + if (missingCapabilities.length > 0) { + return { + adapter, + attempt: { + kind, + reason: 'missing_capabilities', + missingCapabilities, + diagnosticCodes: diagnosticCodes(probe), + ...(probe.diagnostics.length ? { diagnostics: probe.diagnostics } : {}) + } + } + } + + return { adapter, probe } + } + + private createFallbackDiagnostic( + attempt: AdapterSelectionAttempt, + requiredCapabilities: readonly NetworkCapability[] + ): Diagnostic { + return { + code: 'NND_AUTO_FALLBACK', + level: 'warn', + message: 'Native adapter cannot satisfy this selection; using legacy adapter.', + hint: 'Use mode "native" to fail instead of falling back.', + details: { + from: 'native', + to: 'legacy', + reason: attempt.reason, + requiredCapabilities, + ...(attempt.missingCapabilities + ? { missingCapabilities: attempt.missingCapabilities } + : {}), + ...(attempt.diagnosticCodes ? { diagnosticCodes: attempt.diagnosticCodes } : {}), + ...(attempt.diagnostics ? { diagnostics: attempt.diagnostics } : {}), + ...(attempt.error ? { error: attempt.error } : {}) + } + } + } +} diff --git a/packages/network-debugger/src/adapters/types.ts b/packages/network-debugger/src/adapters/types.ts new file mode 100644 index 0000000..4582d48 --- /dev/null +++ b/packages/network-debugger/src/adapters/types.ts @@ -0,0 +1,81 @@ +export const NETWORK_CAPABILITIES = [ + 'http', + 'https', + 'fetch', + 'http2', + 'responseBody', + 'requestBody', + 'websocketLifecycle', + 'websocketFrames', + 'sseMessages', + 'initiator' +] as const + +export type NetworkCapability = (typeof NETWORK_CAPABILITIES)[number] +export type CapabilityMap = Readonly> +export type AdapterKind = 'native' | 'legacy' +export type AdapterMode = 'auto' | AdapterKind + +export type DiagnosticLevel = 'info' | 'warn' | 'error' + +export interface Diagnostic { + code: string + level: DiagnosticLevel + message: string + hint?: string + details?: Readonly> +} + +export interface DevtoolsTarget { + id: string + title: string + type: string + url: string + webSocketDebuggerUrl: string + devtoolsFrontendUrl?: string + devtoolsFrontendUrlCompat?: string + discoveryUrl: string +} + +export interface InspectorTargetOptions { + host?: string + port?: number +} + +export interface AdapterStartOptions { + inspector?: InspectorTargetOptions + requiredCapabilities?: readonly NetworkCapability[] +} + +export interface AdapterProbe { + kind: AdapterKind + available: boolean + /** Whether Auto mode may select this adapter as a proven default baseline. */ + autoSelectable?: boolean + capabilities: CapabilityMap + diagnostics: readonly Diagnostic[] +} + +export interface AdapterSession { + kind: AdapterKind + capabilities: CapabilityMap + target: DevtoolsTarget + diagnostics: readonly Diagnostic[] + /** Optional live diagnostics emitted after the initial target is ready. */ + onDiagnostic?(listener: (diagnostic: Diagnostic) => void): () => void + /** Optional terminal backend failure emitted after initial readiness. */ + onFailure?(listener: (error: Error) => void): () => void + dispose(): Promise +} + +export interface DebugAdapter { + readonly kind: AdapterKind + probe(options?: AdapterStartOptions): AdapterProbe | Promise + start(options?: AdapterStartOptions): Promise +} + +export interface AdapterSelection { + adapter: DebugAdapter + probe: AdapterProbe + fallbackReason?: Diagnostic +} diff --git a/packages/network-debugger/src/cli/args.test.ts b/packages/network-debugger/src/cli/args.test.ts new file mode 100644 index 0000000..2e6efe4 --- /dev/null +++ b/packages/network-debugger/src/cli/args.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest' +import { parseCliArgs } from './args' + +describe('CLI argument parsing', () => { + it('parses the documented dev syntax and preserves application arguments', () => { + expect( + parseCliArgs([ + 'dev', + '--open', + '--no-wait', + '--watch', + '--runner', + 'tsx', + '--mode=native', + '--inspect-port', + '0', + '--require=responseBody', + 'src/app.ts', + '--', + '--port', + '3000' + ]) + ).toEqual({ + command: 'dev', + entry: 'src/app.ts', + applicationArgs: ['--port', '3000'], + config: { + open: true, + wait: false, + watch: true, + runner: 'tsx', + mode: 'native', + inspector: { port: 0 }, + requiredCapabilities: ['responseBody'] + } + }) + }) + + it('parses doctor JSON, config, and bounded probe wait', () => { + expect( + parseCliArgs([ + 'doctor', + '--json', + '--probe-wait=1500', + '--config', + 'custom.mjs', + '--mode', + 'legacy' + ]) + ).toEqual({ + command: 'doctor', + json: true, + probeWaitMs: 1500, + configFile: 'custom.mjs', + config: { mode: 'legacy' } + }) + }) + + it('returns help and version commands', () => { + expect(parseCliArgs([])).toEqual({ command: 'help' }) + expect(parseCliArgs(['--version'])).toEqual({ command: 'version' }) + }) + + it('parses replay dry-run and execution controls', () => { + expect( + parseCliArgs([ + 'replay', + '--dry-run', + '--json', + '--stop-on-error', + '--timeout=2500', + '.nnd/session' + ]) + ).toEqual({ + command: 'replay', + source: '.nnd/session', + dryRun: true, + stopOnError: true, + timeoutMs: 2500, + json: true + }) + }) + + it('uses stable errors for missing entries and invalid flags', () => { + expect(() => parseCliArgs(['dev', '--open'])).toThrowError( + expect.objectContaining({ code: 'NND_CLI_USAGE' }) + ) + expect(() => parseCliArgs(['dev', '--runner', 'bun', 'app.js'])).toThrowError( + expect.objectContaining({ code: 'NND_CLI_INVALID_OPTION' }) + ) + expect(() => parseCliArgs(['doctor', '--probe-wait', '-1'])).toThrowError( + expect.objectContaining({ code: 'NND_CLI_INVALID_OPTION' }) + ) + expect(() => parseCliArgs(['replay', '--dry-run'])).toThrowError( + expect.objectContaining({ code: 'NND_CLI_USAGE' }) + ) + }) +}) diff --git a/packages/network-debugger/src/cli/args.ts b/packages/network-debugger/src/cli/args.ts new file mode 100644 index 0000000..ea6924a --- /dev/null +++ b/packages/network-debugger/src/cli/args.ts @@ -0,0 +1,348 @@ +import type { AdapterMode, NetworkCapability } from '../adapters/types' +import { NETWORK_CAPABILITIES } from '../adapters/types' +import type { NndConfig, NndRunner } from '../config' +import { NndCliError } from './errors' + +export interface DevInvocation { + command: 'dev' + entry: string + applicationArgs: readonly string[] + config: NndConfig + configFile?: string +} + +export interface DoctorInvocation { + command: 'doctor' + json: boolean + probeWaitMs: number + config: NndConfig + configFile?: string +} + +export interface ReplayInvocation { + command: 'replay' + source: string + dryRun: boolean + stopOnError: boolean + timeoutMs?: number + json: boolean +} + +export type CliInvocation = + | DevInvocation + | DoctorInvocation + | ReplayInvocation + | { command: 'help' } + | { command: 'version' } + +const MODES = new Set(['auto', 'native', 'legacy']) +const RUNNERS = new Set(['node', 'tsx']) +const CAPABILITIES = new Set(NETWORK_CAPABILITIES) + +interface ParsedOption { + name: string + inlineValue?: string +} + +function splitOption(argument: string): ParsedOption { + const equals = argument.indexOf('=') + if (equals === -1) return { name: argument } + return { name: argument.slice(0, equals), inlineValue: argument.slice(equals + 1) } +} + +function requiredValue( + option: ParsedOption, + args: readonly string[], + index: number +): { value: string; consumed: number } { + if (option.inlineValue !== undefined) { + if (!option.inlineValue) { + throw new NndCliError('NND_CLI_USAGE', `${option.name} requires a value.`) + } + return { value: option.inlineValue, consumed: 0 } + } + const value = args[index + 1] + if (value === undefined || value === '--') { + throw new NndCliError('NND_CLI_USAGE', `${option.name} requires a value.`) + } + return { value, consumed: 1 } +} + +function parseNonNegativeInteger(value: string, option: string, maximum?: number): number { + const number = Number(value) + if (!Number.isInteger(number) || number < 0 || (maximum !== undefined && number > maximum)) { + throw new NndCliError( + 'NND_CLI_INVALID_OPTION', + `${option} must be an integer from 0${maximum === undefined ? '' : ` to ${maximum}`}.`, + { option, value } + ) + } + return number +} + +interface CommonParseState { + config: NndConfig + configFile?: string +} + +function applyCommonOption( + option: ParsedOption, + args: readonly string[], + index: number, + state: CommonParseState +): number | undefined { + switch (option.name) { + case '--open': + state.config.open = true + return 0 + case '--no-open': + state.config.open = false + return 0 + case '--wait': + state.config.wait = true + return 0 + case '--no-wait': + state.config.wait = false + return 0 + case '--watch': + state.config.watch = true + return 0 + case '--no-watch': + state.config.watch = false + return 0 + case '--runner': { + const parsed = requiredValue(option, args, index) + if (!RUNNERS.has(parsed.value as NndRunner)) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--runner must be node or tsx.', { + value: parsed.value + }) + } + state.config.runner = parsed.value as NndRunner + return parsed.consumed + } + case '--mode': { + const parsed = requiredValue(option, args, index) + if (!MODES.has(parsed.value as AdapterMode)) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--mode must be auto, native, or legacy.', { + value: parsed.value + }) + } + state.config.mode = parsed.value as AdapterMode + return parsed.consumed + } + case '--config': { + const parsed = requiredValue(option, args, index) + state.configFile = parsed.value + return parsed.consumed + } + case '--inspect-host': { + const parsed = requiredValue(option, args, index) + if (!parsed.value) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--inspect-host cannot be empty.') + } + state.config.inspector = { ...state.config.inspector, host: parsed.value } + return parsed.consumed + } + case '--inspect-port': { + const parsed = requiredValue(option, args, index) + state.config.inspector = { + ...state.config.inspector, + port: parseNonNegativeInteger(parsed.value, '--inspect-port', 65_535) + } + return parsed.consumed + } + case '--require': + case '--required-capability': { + const parsed = requiredValue(option, args, index) + if (!CAPABILITIES.has(parsed.value)) { + throw new NndCliError( + 'NND_CLI_INVALID_OPTION', + `Unknown network capability: ${parsed.value}.`, + { value: parsed.value, allowed: [...NETWORK_CAPABILITIES] } + ) + } + state.config.requiredCapabilities = [ + ...(state.config.requiredCapabilities ?? []), + parsed.value as NetworkCapability + ] + return parsed.consumed + } + default: + return undefined + } +} + +function parseDev(args: readonly string[]): DevInvocation { + const state: CommonParseState = { config: {} } + let entry: string | undefined + let applicationArgs: string[] = [] + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index] + if (argument === '--') { + if (!entry) { + entry = args[index + 1] + if (!entry) break + applicationArgs = args.slice(index + 2) + } else { + applicationArgs = args.slice(index + 1) + } + break + } + + if (!entry && argument.startsWith('-')) { + if (argument === '-h' || argument === '--help') { + throw new NndCliError('NND_CLI_USAGE', 'Use `nnd help` for usage.') + } + const option = splitOption(argument) + const consumed = applyCommonOption(option, args, index, state) + if (consumed === undefined) { + throw new NndCliError('NND_CLI_INVALID_OPTION', `Unknown option: ${option.name}.`, { + option: option.name + }) + } + index += consumed + continue + } + + if (!entry) { + entry = argument + } else { + applicationArgs = args.slice(index) + break + } + } + + if (!entry) { + throw new NndCliError('NND_CLI_USAGE', 'nnd dev requires an entry file.') + } + + return { + command: 'dev', + entry, + applicationArgs, + config: state.config, + ...(state.configFile ? { configFile: state.configFile } : {}) + } +} + +function parseDoctor(args: readonly string[]): DoctorInvocation { + const state: CommonParseState = { config: {} } + let json = false + let probeWaitMs = 0 + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index] + const option = splitOption(argument) + if (option.name === '--json') { + if (option.inlineValue !== undefined) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--json does not accept a value.') + } + json = true + continue + } + if (option.name === '--probe-wait') { + const parsed = requiredValue(option, args, index) + probeWaitMs = parseNonNegativeInteger(parsed.value, '--probe-wait', 60_000) + index += parsed.consumed + continue + } + + const consumed = applyCommonOption(option, args, index, state) + if (consumed === undefined) { + throw new NndCliError('NND_CLI_INVALID_OPTION', `Unknown doctor option: ${option.name}.`, { + option: option.name + }) + } + index += consumed + } + + return { + command: 'doctor', + json, + probeWaitMs, + config: state.config, + ...(state.configFile ? { configFile: state.configFile } : {}) + } +} + +function parseReplay(args: readonly string[]): ReplayInvocation { + let source: string | undefined + let dryRun = false + let stopOnError = false + let timeoutMs: number | undefined + let json = false + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index] + if (!source && argument.startsWith('-')) { + const option = splitOption(argument) + if (option.name === '--dry-run') { + if (option.inlineValue !== undefined) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--dry-run does not accept a value.') + } + dryRun = true + continue + } + if (option.name === '--stop-on-error') { + if (option.inlineValue !== undefined) { + throw new NndCliError( + 'NND_CLI_INVALID_OPTION', + '--stop-on-error does not accept a value.' + ) + } + stopOnError = true + continue + } + if (option.name === '--json') { + if (option.inlineValue !== undefined) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--json does not accept a value.') + } + json = true + continue + } + if (option.name === '--timeout') { + const parsed = requiredValue(option, args, index) + timeoutMs = parseNonNegativeInteger(parsed.value, '--timeout', 3_600_000) + if (timeoutMs === 0) { + throw new NndCliError('NND_CLI_INVALID_OPTION', '--timeout must be greater than zero.') + } + index += parsed.consumed + continue + } + throw new NndCliError('NND_CLI_INVALID_OPTION', `Unknown replay option: ${option.name}.`, { + option: option.name + }) + } + if (source) { + throw new NndCliError('NND_CLI_USAGE', 'nnd replay accepts exactly one source path.') + } + source = argument + } + + if (!source) { + throw new NndCliError('NND_CLI_USAGE', 'nnd replay requires a Session directory or HAR file.') + } + return { + command: 'replay', + source, + dryRun, + stopOnError, + ...(timeoutMs !== undefined ? { timeoutMs } : {}), + json + } +} + +export function parseCliArgs(args: readonly string[]): CliInvocation { + const [command, ...rest] = args + if (!command || command === 'help' || command === '--help' || command === '-h') { + return { command: 'help' } + } + if (command === '--version' || command === '-v' || command === 'version') { + return { command: 'version' } + } + if (command === 'dev') return parseDev(rest) + if (command === 'doctor') return parseDoctor(rest) + if (command === 'replay') return parseReplay(rest) + throw new NndCliError('NND_CLI_USAGE', `Unknown command: ${command}.`, { command }) +} diff --git a/packages/network-debugger/src/cli/bin.ts b/packages/network-debugger/src/cli/bin.ts new file mode 100644 index 0000000..0451851 --- /dev/null +++ b/packages/network-debugger/src/cli/bin.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { runCli } from './main' + +void runCli(process.argv.slice(2)).then((exitCode) => { + process.exitCode = exitCode +}) diff --git a/packages/network-debugger/src/cli/dev.test.ts b/packages/network-debugger/src/cli/dev.test.ts new file mode 100644 index 0000000..6925953 --- /dev/null +++ b/packages/network-debugger/src/cli/dev.test.ts @@ -0,0 +1,371 @@ +import { EventEmitter } from 'node:events' +import { PassThrough } from 'node:stream' +import type { ChildProcess } from 'node:child_process' +import { describe, expect, it, vi } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import type { ResolvedNndConfig } from '../config' +import { NND_PRELOAD_CONFIG_ENV } from '../config' +import { NND_READY_PREFIX } from '../preload' +import { + InspectorUrlParser, + ReadyMessageParser, + buildDevCommand, + runDevCommand, + shouldLaunchInspector, + type DevCommand +} from './dev' + +function config(overrides: Partial = {}): ResolvedNndConfig { + return { + mode: 'auto', + open: false, + wait: true, + watch: false, + runner: 'node', + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: [], + legacy: {}, + ...overrides + } +} + +function fakeChild() { + const child = new EventEmitter() as ChildProcess + Object.assign(child, { + stderr: new PassThrough(), + kill: vi.fn(() => true) + }) + return child +} + +const target = (url: string): DevtoolsTarget => ({ + id: url.split('/').at(-1)!, + title: 'target', + type: 'node', + url: '', + webSocketDebuggerUrl: url, + discoveryUrl: 'http://127.0.0.1/json/list', + devtoolsFrontendUrl: `devtools://devtools/bundled/inspector.html?ws=${url.slice(5)}` +}) + +function command(overrides: Partial = {}): DevCommand { + return { + executable: '/node', + args: ['app.js'], + cwd: '/project', + env: {}, + open: false, + wait: false, + ...overrides + } +} + +const nativeRuntime = { + nodeVersion: '24.7.0', + inspectorAvailable: true, + inspectorNetwork: { + requestWillBeSent: vi.fn(), + responseReceived: vi.fn(), + loadingFinished: vi.fn(), + loadingFailed: vi.fn() + } +} as const + +describe('dev command construction', () => { + it('injects native flags and the absolute preload URL without changing NODE_OPTIONS', () => { + const built = buildDevCommand({ + entry: 'src/app.ts', + applicationArgs: ['--port', '3000'], + config: config({ open: true, watch: true, runner: 'tsx' }), + cwd: '/project', + env: { NODE_OPTIONS: '--trace-warnings' }, + execPath: '/usr/bin/node', + preloadUrl: 'file:///package/dist/register.mjs', + ...nativeRuntime + }) + + expect(built).toMatchObject({ + executable: '/usr/bin/node', + cwd: '/project', + open: true, + wait: true + }) + expect(built.args).toEqual([ + '--experimental-network-inspection', + '--inspect-wait=127.0.0.1:0', + '--watch', + '--import=file:///package/dist/register.mjs', + '--import=tsx', + 'src/app.ts', + '--port', + '3000' + ]) + expect(built.env.NODE_OPTIONS).toBe('--trace-warnings') + expect(built.env.NODE_OPTIONS).not.toContain('experimental-network-inspection') + expect(JSON.parse(built.env[NND_PRELOAD_CONFIG_ENV]!)).toMatchObject({ mode: 'auto' }) + }) + + it('uses --inspect for no-wait and omits all Inspector flags for forced Legacy', () => { + expect( + buildDevCommand({ + entry: 'app.js', + config: config({ wait: false }), + preloadUrl: 'file:///register.mjs', + ...nativeRuntime + }).args + ).toEqual([ + '--experimental-network-inspection', + '--inspect=127.0.0.1:0', + '--import=file:///register.mjs', + 'app.js' + ]) + + const legacy = buildDevCommand({ + entry: 'app.js', + config: config({ mode: 'legacy', wait: true }), + preloadUrl: 'file:///register.mjs' + }) + expect(legacy.args).toEqual(['--import=file:///register.mjs', 'app.js']) + expect(legacy.wait).toBe(false) + }) + + it('starts Auto directly on Legacy when the native baseline or requirements are unmet', () => { + const oldRuntime = buildDevCommand({ + entry: 'app.js', + config: config(), + nodeVersion: '18.20.8', + preloadUrl: 'file:///register.mjs' + }) + expect(oldRuntime.args).toEqual(['--import=file:///register.mjs', 'app.js']) + expect(oldRuntime.wait).toBe(false) + + const missingCapability = buildDevCommand({ + entry: 'app.js', + config: config({ requiredCapabilities: ['requestBody'] }), + nodeVersion: '24.16.0', + inspectorNetwork: { + requestWillBeSent: vi.fn(), + responseReceived: vi.fn(), + loadingFinished: vi.fn(), + loadingFailed: vi.fn(), + dataReceived: vi.fn() + }, + preloadUrl: 'file:///register.mjs' + }) + expect(missingCapability.args).toEqual(['--import=file:///register.mjs', 'app.js']) + expect(missingCapability.wait).toBe(false) + }) + + it('uses the shared Native Auto baseline at Node 18/20/24.6/24.7 boundaries', () => { + const network = { + requestWillBeSent: vi.fn(), + responseReceived: vi.fn(), + loadingFinished: vi.fn(), + loadingFailed: vi.fn(), + dataReceived: vi.fn() + } + for (const nodeVersion of ['18.20.8', '20.19.5', '24.6.0']) { + expect(shouldLaunchInspector(config(), { nodeVersion, inspectorNetwork: network })).toBe( + false + ) + } + expect( + shouldLaunchInspector(config(), { + nodeVersion: '24.7.0', + inspectorNetwork: network, + inspectorAvailable: true + }) + ).toBe(true) + expect( + shouldLaunchInspector(config(), { + nodeVersion: '24.7.0', + inspectorNetwork: network, + inspectorAvailable: false + }) + ).toBe(false) + }) + + it('rejects forced Native before spawn when the runtime does not recognize the flag', () => { + for (const nodeVersion of ['18.20.8', '20.17.0', '21.7.3', '22.5.1']) { + expect(() => + buildDevCommand({ + entry: 'app.js', + config: config({ mode: 'native' }), + nodeVersion, + preloadUrl: 'file:///register.mjs' + }) + ).toThrowError( + expect.objectContaining({ + code: 'NND_CLI_NATIVE_UNSUPPORTED', + message: expect.stringContaining('use --mode auto/legacy') + }) + ) + } + }) + + it('injects forced Native flags at the Node 20.18 and 22.6 support boundaries', () => { + for (const nodeVersion of ['20.18.0', '22.6.0']) { + const native = buildDevCommand({ + entry: 'app.js', + config: config({ mode: 'native' }), + nodeVersion, + preloadUrl: 'file:///register.mjs' + }) + expect(native.args.slice(0, 2)).toEqual([ + '--experimental-network-inspection', + '--inspect-wait=127.0.0.1:0' + ]) + } + }) +}) + +describe('stderr protocol parsers', () => { + it('waits for a complete Debugger listening line across chunks', () => { + const parser = new InspectorUrlParser() + expect(parser.push('Debugger listening on ws://127.0.0.1:1234/')).toEqual([]) + expect(parser.push('abc\nFor help, see docs\n')).toEqual(['ws://127.0.0.1:1234/abc']) + expect( + parser.push(`${NND_READY_PREFIX}{"target":{"webSocketDebuggerUrl":"ws://wrong"}}\n`) + ).toEqual([]) + }) + + it('parses split ready messages and ignores malformed data', () => { + const parser = new ReadyMessageParser() + const ready = { + mode: 'legacy', + target: target('ws://127.0.0.1:5271'), + capabilities: { http: true } + } + const line = `${NND_READY_PREFIX}${JSON.stringify(ready)}\n` + expect(parser.push(line.slice(0, 20))).toEqual([]) + expect(parser.push(line.slice(20))).toEqual([ready]) + expect(parser.push(`${NND_READY_PREFIX}{nope}\n`)).toEqual([]) + }) +}) + +describe('dev child lifecycle', () => { + it('forwards parent signals, maps signal exit codes, and removes listeners', async () => { + const child = fakeChild() + const signals = new EventEmitter() + const result = runDevCommand(command(), { + spawn: vi.fn(() => child), + stderr: { write: vi.fn(() => true) }, + signals + }) + + signals.emit('SIGINT') + expect(child.kill).toHaveBeenCalledWith('SIGINT') + child.emit('exit', null, 'SIGINT') + await expect(result).resolves.toBe(130) + expect(signals.listenerCount('SIGINT')).toBe(0) + expect(signals.listenerCount('SIGTERM')).toBe(0) + expect(signals.listenerCount('SIGHUP')).toBe(0) + }) + + it('prints an actionable status when default wait has no automatic frontend', async () => { + const child = fakeChild() + const stderr = { write: vi.fn(() => true) } + const result = runDevCommand(command({ wait: true }), { + spawn: vi.fn(() => child), + stderr, + signals: new EventEmitter() + }) + expect(stderr.write).toHaveBeenCalledWith(expect.stringContaining('NND_WAITING_FOR_FRONTEND')) + child.emit('exit', 0, null) + await expect(result).resolves.toBe(0) + }) + + it('opens the preliminary waiting target once, then a different fallback ready target', async () => { + const child = fakeChild() + const openInspector = vi.fn(async () => undefined) + const openTarget = vi.fn(async () => undefined) + const result = runDevCommand(command({ open: true, wait: true }), { + spawn: vi.fn(() => child), + stderr: { write: vi.fn(() => true) }, + signals: new EventEmitter(), + openInspector, + openTarget + }) + const nativeUrl = 'ws://127.0.0.1:1234/native' + child.stderr!.emit('data', `Debugger listening on ${nativeUrl}\n`) + child.stderr!.emit( + 'data', + `${NND_READY_PREFIX}${JSON.stringify({ + mode: 'native', + target: target(nativeUrl), + capabilities: { http: true } + })}\n` + ) + const legacyTarget = target('ws://127.0.0.1:5271/legacy') + child.stderr!.emit( + 'data', + `${NND_READY_PREFIX}${JSON.stringify({ + mode: 'legacy', + target: legacyTarget, + capabilities: { http: true } + })}\n` + ) + + await Promise.resolve() + expect(openInspector).toHaveBeenCalledTimes(1) + expect(openInspector).toHaveBeenCalledWith(nativeUrl) + expect(openTarget).toHaveBeenCalledTimes(1) + expect(openTarget).toHaveBeenCalledWith(legacyTarget) + child.emit('exit', 0, null) + await expect(result).resolves.toBe(0) + }) + + it('opens only the authoritative ready target in Legacy/no-wait mode', async () => { + const child = fakeChild() + const openInspector = vi.fn(async () => undefined) + const openTarget = vi.fn(async () => undefined) + const result = runDevCommand(command({ open: true, wait: false }), { + spawn: vi.fn(() => child), + stderr: { write: vi.fn(() => true) }, + signals: new EventEmitter(), + openInspector, + openTarget + }) + const readyTarget = target('ws://127.0.0.1:5271/legacy') + child.stderr!.emit('data', `Debugger listening on ws://127.0.0.1:1234/native\n`) + child.stderr!.emit( + 'data', + `${NND_READY_PREFIX}${JSON.stringify({ + mode: 'legacy', + target: readyTarget, + capabilities: { http: true } + })}\n` + ) + await Promise.resolve() + + expect(openInspector).not.toHaveBeenCalled() + expect(openTarget).toHaveBeenCalledWith(readyTarget) + child.emit('exit', 0, null) + await expect(result).resolves.toBe(0) + }) + + it('returns one and terminates the child when frontend launch fails', async () => { + const child = fakeChild() + const result = runDevCommand(command({ open: true, wait: false }), { + spawn: vi.fn(() => child), + stderr: { write: vi.fn(() => true) }, + signals: new EventEmitter(), + openTarget: vi.fn(async () => { + throw new Error('browser failed') + }) + }) + child.stderr!.emit( + 'data', + `${NND_READY_PREFIX}${JSON.stringify({ + mode: 'native', + target: target('ws://127.0.0.1:1234/native'), + capabilities: { http: true } + })}\n` + ) + await Promise.resolve() + await Promise.resolve() + expect(child.kill).toHaveBeenCalledWith('SIGTERM') + child.emit('exit', null, 'SIGTERM') + await expect(result).resolves.toBe(1) + }) +}) diff --git a/packages/network-debugger/src/cli/dev.ts b/packages/network-debugger/src/cli/dev.ts new file mode 100644 index 0000000..dff6c8e --- /dev/null +++ b/packages/network-debugger/src/cli/dev.ts @@ -0,0 +1,328 @@ +import { spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from 'node:child_process' +import * as nodeInspector from 'node:inspector' +import type { Writable } from 'node:stream' +import { discoverInspectorTarget } from '../adapters/node-native' +import { + NATIVE_NETWORK_INSPECTION_FLAG, + getNativeCapabilities, + hasRequiredNativeMethods, + isNativeAutoBaseline, + parseNodeVersion, + supportsNativeNetworkInspection, + type NativeNetworkApi +} from '../adapters/node-native/capability' +import { NND_PRELOAD_CONFIG_ENV, serializePreloadConfig, type ResolvedNndConfig } from '../config' +import type { DevtoolsTarget } from '../adapters/types' +import { NND_PRELOAD_REPORT_ENV, NND_READY_PREFIX } from '../preload' +import { openDevtoolsTarget } from '../target/frontend-launcher' +import { NndCliError, formatCliError } from './errors' + +export interface DevCommand { + executable: string + args: readonly string[] + cwd: string + env: NodeJS.ProcessEnv + open: boolean + wait: boolean +} + +export interface BuildDevCommandOptions { + entry: string + applicationArgs?: readonly string[] + config: ResolvedNndConfig + cwd?: string + env?: NodeJS.ProcessEnv + execPath?: string + preloadUrl?: string + nodeVersion?: string + inspectorNetwork?: NativeNetworkApi + inspectorAvailable?: boolean +} + +export function defaultPreloadUrl(moduleUrl: string = import.meta.url): string { + return new URL('./register.mjs', moduleUrl).href +} + +export function shouldLaunchInspector( + config: ResolvedNndConfig, + options: Pick< + BuildDevCommandOptions, + 'nodeVersion' | 'inspectorNetwork' | 'inspectorAvailable' + > = {} +): boolean { + if (config.mode === 'legacy') return false + if (config.mode === 'native') return true + + const version = parseNodeVersion(options.nodeVersion ?? process.versions.node) + if (!isNativeAutoBaseline(version)) return false + const inspectorAvailable = options.inspectorAvailable ?? process.features?.inspector !== false + if (!inspectorAvailable) return false + const network = + options.inspectorNetwork ?? (nodeInspector as unknown as { Network?: NativeNetworkApi }).Network + if (!hasRequiredNativeMethods(network)) return false + const capabilities = getNativeCapabilities(version, network) + return config.requiredCapabilities.every((capability) => capabilities[capability]) +} + +export function buildDevCommand(options: BuildDevCommandOptions): DevCommand { + const { config } = options + const nodeVersionText = options.nodeVersion ?? process.versions.node + if ( + config.mode === 'native' && + !supportsNativeNetworkInspection(parseNodeVersion(nodeVersionText)) + ) { + throw new NndCliError( + 'NND_CLI_NATIVE_UNSUPPORTED', + `Node.js ${nodeVersionText} does not support ${NATIVE_NETWORK_INSPECTION_FLAG}. Upgrade to Node.js 20.18+, 22.6+, or a newer release, or use --mode auto/legacy.`, + { + nodeVersion: nodeVersionText, + flag: NATIVE_NETWORK_INSPECTION_FLAG, + supportedBaselines: ['20.18+', '22.6+', '23+'] + } + ) + } + const inspectorAddress = `${config.inspector.host}:${config.inspector.port}` + const usesInspector = shouldLaunchInspector(config, options) + const args = usesInspector + ? [ + NATIVE_NETWORK_INSPECTION_FLAG, + `${config.wait ? '--inspect-wait' : '--inspect'}=${inspectorAddress}` + ] + : [] + + if (config.watch) args.push('--watch') + args.push(`--import=${options.preloadUrl ?? defaultPreloadUrl()}`) + if (config.runner === 'tsx') args.push('--import=tsx') + args.push(options.entry, ...(options.applicationArgs ?? [])) + + return { + executable: options.execPath ?? process.execPath, + args, + cwd: options.cwd ?? process.cwd(), + env: { + ...(options.env ?? process.env), + [NND_PRELOAD_CONFIG_ENV]: serializePreloadConfig(config), + [NND_PRELOAD_REPORT_ENV]: '1' + }, + open: config.open, + wait: usesInspector && config.wait + } +} + +type SupportedSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP' + +const SIGNAL_EXIT_CODES: Readonly> = { + SIGINT: 130, + SIGTERM: 143, + SIGHUP: 129 +} + +export interface SignalSource { + on(event: SupportedSignal, listener: () => void): unknown + off(event: SupportedSignal, listener: () => void): unknown +} + +export interface RunDevDependencies { + spawn?: (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess + stderr?: Pick + signals?: SignalSource + openInspector?: (inspectorUrl: string) => Promise + openTarget?: (target: DevtoolsTarget) => Promise +} + +async function defaultOpenInspector(inspectorUrl: string): Promise { + const target = await discoverInspectorTarget(inspectorUrl) + await openDevtoolsTarget(target) +} + +/** Extracts complete Inspector WebSocket URLs even when stderr arrives in chunks. */ +export class InspectorUrlParser { + private pending = '' + private emitted = new Set() + + push(chunk: string): readonly string[] { + this.pending = `${this.pending}${chunk}`.slice(-8192) + const lastNewline = this.pending.lastIndexOf('\n') + if (lastNewline < 0) return [] + const complete = this.pending.slice(0, lastNewline + 1) + this.pending = this.pending.slice(lastNewline + 1) + const found = complete + .split(/\r?\n/) + .filter((line) => line.includes('Debugger listening on ')) + .flatMap((line) => line.match(/ws:\/\/[^\s]+/g) ?? []) + const fresh = found + .map((url) => url.replace(/[),.;]+$/, '')) + .filter((url) => { + if (this.emitted.has(url)) return false + this.emitted.add(url) + return true + }) + return fresh + } +} + +export interface NndReadyMessage { + mode: 'native' | 'legacy' + target: DevtoolsTarget + capabilities: Readonly> + fallbackReason?: unknown +} + +export class ReadyMessageParser { + private pending = '' + + push(chunk: string): readonly NndReadyMessage[] { + this.pending = `${this.pending}${chunk}`.slice(-65_536) + const lastNewline = this.pending.lastIndexOf('\n') + if (lastNewline < 0) return [] + const lines = this.pending.slice(0, lastNewline + 1).split(/\r?\n/) + this.pending = this.pending.slice(lastNewline + 1) + const messages: NndReadyMessage[] = [] + for (const line of lines) { + const index = line.indexOf(NND_READY_PREFIX) + if (index < 0) continue + try { + const value = JSON.parse(line.slice(index + NND_READY_PREFIX.length)) as NndReadyMessage + if ( + (value.mode === 'native' || value.mode === 'legacy') && + value.target && + typeof value.target.webSocketDebuggerUrl === 'string' && + value.capabilities && + typeof value.capabilities === 'object' + ) { + messages.push(value) + } + } catch { + // The original stderr line remains visible; malformed status data must + // not crash or hide the child process. + } + } + return messages + } +} + +export function runDevCommand( + command: DevCommand, + dependencies: RunDevDependencies = {} +): Promise { + const spawn = dependencies.spawn ?? nodeSpawn + const stderr = dependencies.stderr ?? process.stderr + const signals = dependencies.signals ?? process + const openInspector = dependencies.openInspector ?? defaultOpenInspector + const openTarget = dependencies.openTarget ?? openDevtoolsTarget + if (command.wait && !command.open) { + stderr.write( + '[nnd:NND_WAITING_FOR_FRONTEND] Application entry is paused; attach to the Inspector URL or rerun with --open/--no-wait.\n' + ) + } + let child: ChildProcess + + try { + child = spawn(command.executable, command.args, { + cwd: command.cwd, + env: command.env, + stdio: ['inherit', 'inherit', 'pipe'] + }) + } catch (error) { + return Promise.reject( + new NndCliError( + 'NND_CLI_SPAWN_FAILED', + `Unable to start ${command.executable}.`, + { executable: command.executable }, + error + ) + ) + } + + return new Promise((resolvePromise, rejectPromise) => { + const urlParser = new InspectorUrlParser() + const readyParser = new ReadyMessageParser() + let requestedSignal: SupportedSignal | undefined + let frontendError = false + let settled = false + const openedTargets = new Set() + + const signalListeners = new Map void>() + const cleanup = () => { + for (const [signal, listener] of signalListeners) signals.off(signal, listener) + signalListeners.clear() + } + const settle = (action: () => void) => { + if (settled) return + settled = true + cleanup() + action() + } + + for (const signal of Object.keys(SIGNAL_EXIT_CODES) as SupportedSignal[]) { + const listener = () => { + requestedSignal = signal + child.kill(signal) + } + signalListeners.set(signal, listener) + signals.on(signal, listener) + } + + child.stderr?.on('data', (chunk: Buffer | string) => { + const text = chunk.toString() + stderr.write(text) + const inspectorUrls = urlParser.push(text) + const readyMessages = readyParser.push(text) + if (!command.open) return + + const open = (key: string, action: () => Promise) => { + if (openedTargets.has(key)) return + openedTargets.add(key) + void action().catch((error) => { + frontendError = true + stderr.write( + `${formatCliError( + new NndCliError( + 'NND_CLI_FRONTEND_OPEN_FAILED', + `Unable to open DevTools for ${key}.`, + { target: key }, + error + ) + )}\n` + ) + child.kill('SIGTERM') + }) + } + + // A waiting Native/Auto process must first be attached so preload can + // execute. No-wait and Legacy modes wait for the authoritative ready target. + if (command.wait) { + for (const inspectorUrl of inspectorUrls) { + open(inspectorUrl, () => openInspector(inspectorUrl)) + } + } + for (const ready of readyMessages) { + open(ready.target.webSocketDebuggerUrl, () => openTarget(ready.target)) + } + }) + + child.once('error', (error) => { + settle(() => + rejectPromise( + new NndCliError( + 'NND_CLI_SPAWN_FAILED', + `Unable to start ${command.executable}: ${error.message}`, + { executable: command.executable }, + error + ) + ) + ) + }) + child.once('exit', (code, signal) => { + settle(() => { + if (frontendError) return resolvePromise(1) + if (typeof code === 'number') return resolvePromise(code) + if (requestedSignal) return resolvePromise(SIGNAL_EXIT_CODES[requestedSignal]) + if (signal && signal in SIGNAL_EXIT_CODES) { + return resolvePromise(SIGNAL_EXIT_CODES[signal as SupportedSignal]) + } + return resolvePromise(1) + }) + }) + }) +} diff --git a/packages/network-debugger/src/cli/errors.ts b/packages/network-debugger/src/cli/errors.ts new file mode 100644 index 0000000..60d77de --- /dev/null +++ b/packages/network-debugger/src/cli/errors.ts @@ -0,0 +1,27 @@ +export type NndCliErrorCode = + | 'NND_CLI_USAGE' + | 'NND_CLI_INVALID_OPTION' + | 'NND_CLI_NATIVE_UNSUPPORTED' + | 'NND_CLI_SPAWN_FAILED' + | 'NND_CLI_FRONTEND_OPEN_FAILED' + +export class NndCliError extends Error { + constructor( + readonly code: NndCliErrorCode, + message: string, + readonly details: Readonly> = {}, + readonly cause?: unknown + ) { + super(message) + this.name = 'NndCliError' + } +} + +export function formatCliError(error: unknown): string { + const code = + typeof error === 'object' && error !== null && 'code' in error + ? String((error as { code: unknown }).code) + : 'NND_CLI_FAILED' + const message = error instanceof Error ? error.message : String(error) + return `[nnd:${code}] ${message}` +} diff --git a/packages/network-debugger/src/cli/help.ts b/packages/network-debugger/src/cli/help.ts new file mode 100644 index 0000000..157558a --- /dev/null +++ b/packages/network-debugger/src/cli/help.ts @@ -0,0 +1,31 @@ +export const CLI_HELP = `Node Network Devtools + +Usage: + nnd dev [options] [-- args...] + nnd doctor [--json] [--probe-wait ] + nnd replay [--dry-run] [--json] [--timeout ] + +Dev options: + --open Open DevTools when the Inspector target appears + --no-wait Start the application immediately (default: wait) + --watch Restart the application when files change + --runner Select the application runner (default: node) + --mode + --config Use an explicit nnd.config.mjs/cjs/json file + --inspect-host Inspector bind host (default: 127.0.0.1) + --inspect-port Inspector port; 0 asks the OS to choose + --require Require an adapter capability; may be repeated + +Replay options: + --dry-run Validate and print the request plan without I/O + --stop-on-error Stop after the first HTTP or transport failure + --timeout Per-request timeout (default: 30000) + --json Print the complete machine-readable report + +Configuration precedence: + explicit CLI > NND_* environment > config file > defaults + +By default, Native/Auto targets pause before the application entry runs. Attach +a debugger to the printed Inspector URL, pass --open to attach automatically, +or pass --no-wait to run immediately. Forced Legacy mode never starts Inspector. +` diff --git a/packages/network-debugger/src/cli/index.ts b/packages/network-debugger/src/cli/index.ts new file mode 100644 index 0000000..1004dcd --- /dev/null +++ b/packages/network-debugger/src/cli/index.ts @@ -0,0 +1,5 @@ +export * from './args' +export * from './dev' +export * from './errors' +export * from './help' +export * from './main' diff --git a/packages/network-debugger/src/cli/main.test.ts b/packages/network-debugger/src/cli/main.test.ts new file mode 100644 index 0000000..d3d1e0e --- /dev/null +++ b/packages/network-debugger/src/cli/main.test.ts @@ -0,0 +1,177 @@ +import { EventEmitter } from 'node:events' +import { PassThrough } from 'node:stream' +import type { ChildProcess } from 'node:child_process' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { runCli } from './main' + +const temporaryDirectories: string[] = [] + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +function output() { + let value = '' + return { + stream: { write: vi.fn((chunk: unknown) => ((value += String(chunk)), true)) }, + value: () => value + } +} + +describe('runCli', () => { + it('prints help and package version without spawning', async () => { + const stdout = output() + await expect(runCli([], { stdout: stdout.stream, stderr: output().stream })).resolves.toBe(0) + expect(stdout.value()).toContain('nnd dev [options] ') + + const version = output() + await expect( + runCli(['--version'], { + stdout: version.stream, + stderr: output().stream, + packageVersion: '9.8.7' + }) + ).resolves.toBe(0) + expect(version.value()).toBe('9.8.7\n') + }) + + it('prints a machine-readable doctor report', async () => { + const stdout = output() + const exitCode = await runCli(['doctor', '--json', '--mode', 'legacy'], { + stdout: stdout.stream, + stderr: output().stream, + cwd: process.cwd(), + env: {}, + packageVersion: '1.0.30' + }) + const report = JSON.parse(stdout.value()) as { + schemaVersion: number + ok: boolean + selection: { selected: string } + } + expect(exitCode).toBe(0) + expect(report).toMatchObject({ + schemaVersion: 1, + ok: true, + selection: { selected: 'legacy' } + }) + }) + + it('prints a machine-readable replay dry-run without network I/O', async () => { + const cwd = mkdtempSync(join(tmpdir(), 'nnd-cli-replay-')) + temporaryDirectories.push(cwd) + const harPath = join(cwd, 'fixture.har') + writeFileSync( + harPath, + JSON.stringify({ + log: { + version: '1.2', + creator: { name: 'fixture', version: '1' }, + pages: [], + entries: [ + { + request: { + method: 'GET', + url: 'http://127.0.0.1:9/never-opened', + headers: [], + queryString: [], + cookies: [], + headersSize: -1, + bodySize: 0 + }, + response: {}, + cache: {}, + timings: {}, + _requestId: 'dry-run' + } + ] + } + }) + ) + const stdout = output() + + await expect( + runCli(['replay', '--dry-run', '--json', 'fixture.har'], { + cwd, + stdout: stdout.stream, + stderr: output().stream + }) + ).resolves.toBe(0) + expect(JSON.parse(stdout.value())).toMatchObject({ + dryRun: true, + succeeded: 1, + failed: 0, + requests: [{ requestId: 'dry-run', method: 'GET' }] + }) + }) + + it('resolves config and launches the built dev command', async () => { + const child = new EventEmitter() as ChildProcess + Object.assign(child, { stderr: new PassThrough(), kill: vi.fn(() => true) }) + const spawn = vi.fn(() => { + queueMicrotask(() => child.emit('exit', 7, null)) + return child + }) + const stderr = output() + const exitCode = await runCli( + ['dev', '--mode', 'native', '--no-wait', '--runner', 'tsx', 'app.ts', '--', '--port', '3'], + { + cwd: '/project', + env: {}, + execPath: '/node', + nodeVersion: '24.7.0', + preloadUrl: 'file:///dist/register.mjs', + spawn, + signals: new EventEmitter(), + stdout: output().stream, + stderr: stderr.stream + } + ) + + expect(exitCode).toBe(7) + expect(spawn).toHaveBeenCalledWith( + '/node', + [ + '--experimental-network-inspection', + '--inspect=127.0.0.1:0', + '--import=file:///dist/register.mjs', + '--import=tsx', + 'app.ts', + '--port', + '3' + ], + expect.objectContaining({ cwd: '/project' }) + ) + }) + + it('formats parser failures with stable codes', async () => { + const stderr = output() + await expect(runCli(['wat'], { stderr: stderr.stream, stdout: output().stream })).resolves.toBe( + 1 + ) + expect(stderr.value()).toContain('[nnd:NND_CLI_USAGE]') + }) + + it('does not spawn forced Native on a runtime that cannot parse its flag', async () => { + const stderr = output() + const spawn = vi.fn() + const exitCode = await runCli(['dev', '--mode', 'native', 'app.js'], { + cwd: '/project', + env: {}, + nodeVersion: '18.20.8', + spawn, + stdout: output().stream, + stderr: stderr.stream + }) + + expect(exitCode).toBe(1) + expect(spawn).not.toHaveBeenCalled() + expect(stderr.value()).toContain('[nnd:NND_CLI_NATIVE_UNSUPPORTED]') + expect(stderr.value()).toContain('Node.js 18.20.8') + }) +}) diff --git a/packages/network-debugger/src/cli/main.ts b/packages/network-debugger/src/cli/main.ts new file mode 100644 index 0000000..417190a --- /dev/null +++ b/packages/network-debugger/src/cli/main.ts @@ -0,0 +1,100 @@ +import type { Writable } from 'node:stream' +import { resolve } from 'node:path' +import { resolveConfig } from '../config' +import { detectPackageVersion, formatDoctorReport, runDoctor } from '../diagnostics' +import { replay } from '../replay' +import { parseCliArgs } from './args' +import { buildDevCommand, runDevCommand, type RunDevDependencies } from './dev' +import { formatCliError } from './errors' +import { CLI_HELP } from './help' + +export interface RunCliOptions extends RunDevDependencies { + cwd?: string + env?: NodeJS.ProcessEnv + execPath?: string + /** Dependency override used by compatibility checks and tests. */ + nodeVersion?: string + preloadUrl?: string + stdout?: Pick + packageVersion?: string +} + +export async function runCli( + args: readonly string[], + options: RunCliOptions = {} +): Promise { + const stdout = options.stdout ?? process.stdout + const stderr = options.stderr ?? process.stderr + + try { + const invocation = parseCliArgs(args) + if (invocation.command === 'help') { + stdout.write(CLI_HELP) + return 0 + } + if (invocation.command === 'version') { + stdout.write(`${options.packageVersion ?? detectPackageVersion()}\n`) + return 0 + } + if (invocation.command === 'doctor') { + const report = await runDoctor({ + cwd: options.cwd, + env: options.env, + config: invocation.config, + configFile: invocation.configFile, + probeWaitMs: invocation.probeWaitMs, + packageVersion: options.packageVersion + }) + stdout.write(formatDoctorReport(report, invocation.json)) + return report.ok ? 0 : 1 + } + if (invocation.command === 'replay') { + const report = await replay(resolve(options.cwd ?? process.cwd(), invocation.source), { + dryRun: invocation.dryRun, + stopOnError: invocation.stopOnError, + timeoutMs: invocation.timeoutMs + }) + if (invocation.json) { + stdout.write(`${JSON.stringify(report, null, 2)}\n`) + } else { + stdout.write( + `${invocation.dryRun ? 'Planned' : 'Replayed'} ${report.results.length} request(s): ${report.succeeded} succeeded, ${report.failed} failed.\n` + ) + for (const result of report.results) { + const outcome = result.ok + ? (result.status ?? 'ready') + : (result.error ?? result.status ?? 'failed') + stdout.write(`${result.request.method} ${result.request.url} -> ${outcome}\n`) + } + } + return report.failed === 0 ? 0 : 1 + } + + const resolution = await resolveConfig({ + cwd: options.cwd, + env: options.env, + cli: invocation.config, + configFile: invocation.configFile + }) + const command = buildDevCommand({ + entry: invocation.entry, + applicationArgs: invocation.applicationArgs, + config: resolution.config, + cwd: options.cwd, + env: options.env, + execPath: options.execPath, + nodeVersion: options.nodeVersion, + preloadUrl: options.preloadUrl + }) + return await runDevCommand(command, { + spawn: options.spawn, + stderr, + signals: options.signals, + openInspector: options.openInspector, + openTarget: options.openTarget + }) + } catch (error) { + stderr.write(`${formatCliError(error)}\n`) + return 1 + } +} diff --git a/packages/network-debugger/src/common.test.ts b/packages/network-debugger/src/common.test.ts index 80a8137..76ac349 100644 --- a/packages/network-debugger/src/common.test.ts +++ b/packages/network-debugger/src/common.test.ts @@ -1,21 +1,5 @@ import { vi, describe, beforeEach, test, expect } from 'vitest' -import { - RequestDetail, - PORT, - SERVER_PORT, - REMOTE_DEBUGGER_PORT, - IS_DEV_MODE, - READY_MESSAGE, - NETWORK_CONTEXT_KEY, - WS_PROTOCOL, - CONTEXT_KEY_PORT, - CONTEXT_KEY_SERVER_PORT, - CONTEXT_KEY_AUTO_OPEN_DEVTOOL, - CONTEXT_KEY_INTERCEPT_NORMAL, - CONTEXT_KEY_INTERCEPT_FETCH, - CONTEXT_KEY_INTERCEPT_UNDICI_FETCH, - CONTEXT_KEY_HASH -} from './common' +import { RequestDetail } from './common' describe('RequestDetail', () => { beforeEach(() => { @@ -254,164 +238,22 @@ describe('RequestDetail', () => { }) }) - describe('isHiden', () => { - test('should return true for ws://127.0.0.1/ websocket connections', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'ws://127.0.0.1/' - - expect(requestDetail.isHiden()).toBe(true) - }) - - test('should return true for http://127.0.0.1/ websocket connections', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'http://127.0.0.1/' - - expect(requestDetail.isHiden()).toBe(true) - }) - - test('should return false for non-127.0.0.1 websocket connections', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'ws://example.com/' - - expect(requestDetail.isHiden()).toBe(false) - }) - - test('should return false for non-websocket 127.0.0.1 connections', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { 'Content-Type': 'application/json' } - requestDetail.url = 'http://127.0.0.1/' - - expect(requestDetail.isHiden()).toBe(false) - }) - - test('should return false for websocket with different 127.0.0.1 path', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'ws://127.0.0.1/api' - - expect(requestDetail.isHiden()).toBe(false) - }) - - test('should return false for websocket with 127.0.0.1 and port', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'ws://127.0.0.1:8080/' - - expect(requestDetail.isHiden()).toBe(false) - }) - - test('should return false when url is undefined', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = undefined - - expect(requestDetail.isHiden()).toBe(false) - }) - - test('should return false for wss://127.0.0.1/ (secure websocket)', () => { - const requestDetail = new RequestDetail() - requestDetail.requestHeaders = { Upgrade: 'websocket' } - requestDetail.url = 'wss://127.0.0.1/' - - expect(requestDetail.isHiden()).toBe(false) - }) - }) - describe('default property values', () => { - test('should have undefined optional properties by default', () => { + test('initializes mutable header/info containers and leaves scalar fields undefined', () => { const requestDetail = new RequestDetail() expect(requestDetail.url).toBeUndefined() expect(requestDetail.method).toBeUndefined() expect(requestDetail.cookies).toBeUndefined() - expect(requestDetail.requestHeaders).toBeUndefined() + expect(requestDetail.requestHeaders).toEqual({}) expect(requestDetail.requestData).toBeUndefined() expect(requestDetail.responseData).toBeUndefined() expect(requestDetail.responseStatusCode).toBeUndefined() - expect(requestDetail.responseHeaders).toBeUndefined() + expect(requestDetail.responseHeaders).toEqual({}) + expect(requestDetail.responseInfo).toEqual({}) expect(requestDetail.requestStartTime).toBeUndefined() expect(requestDetail.requestEndTime).toBeUndefined() expect(requestDetail.initiator).toBeUndefined() }) }) }) - -describe('常量导出', () => { - describe('端口常量', () => { - test('PORT 应该是数字类型', () => { - expect(typeof PORT).toBe('number') - }) - - test('SERVER_PORT 应该是数字类型', () => { - expect(typeof SERVER_PORT).toBe('number') - }) - - test('REMOTE_DEBUGGER_PORT 应该是数字类型', () => { - expect(typeof REMOTE_DEBUGGER_PORT).toBe('number') - }) - - test('默认端口值应该正确', () => { - // 如果没有设置环境变量,应该使用默认值 - if (!process.env.NETWORK_PORT) { - expect(PORT).toBe(5270) - } - if (!process.env.NETWORK_SERVER_PORT) { - expect(SERVER_PORT).toBe(5271) - } - if (!process.env.REMOTE_DEBUGGER_PORT) { - expect(REMOTE_DEBUGGER_PORT).toBe(9333) - } - }) - }) - - describe('模式常量', () => { - test('IS_DEV_MODE 应该是布尔类型', () => { - expect(typeof IS_DEV_MODE).toBe('boolean') - }) - - test('READY_MESSAGE 应该是字符串 "ready"', () => { - expect(READY_MESSAGE).toBe('ready') - }) - }) - - describe('上下文键常量', () => { - test('NETWORK_CONTEXT_KEY 应该正确', () => { - expect(NETWORK_CONTEXT_KEY).toBe('x-network-context') - }) - - test('WS_PROTOCOL 应该正确', () => { - expect(WS_PROTOCOL).toBe('ws') - }) - - test('CONTEXT_KEY_PORT 应该正确', () => { - expect(CONTEXT_KEY_PORT).toBe('x-network-context-port') - }) - - test('CONTEXT_KEY_SERVER_PORT 应该正确', () => { - expect(CONTEXT_KEY_SERVER_PORT).toBe('x-network-context-server-port') - }) - - test('CONTEXT_KEY_AUTO_OPEN_DEVTOOL 应该正确', () => { - expect(CONTEXT_KEY_AUTO_OPEN_DEVTOOL).toBe('x-network-context-auto-open-devtools') - }) - - test('CONTEXT_KEY_INTERCEPT_NORMAL 应该正确', () => { - expect(CONTEXT_KEY_INTERCEPT_NORMAL).toBe('x-network-context-intercept-normal') - }) - - test('CONTEXT_KEY_INTERCEPT_FETCH 应该正确', () => { - expect(CONTEXT_KEY_INTERCEPT_FETCH).toBe('x-network-context-intercept-fetch') - }) - - test('CONTEXT_KEY_INTERCEPT_UNDICI_FETCH 应该正确', () => { - expect(CONTEXT_KEY_INTERCEPT_UNDICI_FETCH).toBe('x-network-context-intercept-undici-fetch') - }) - - test('CONTEXT_KEY_HASH 应该正确', () => { - expect(CONTEXT_KEY_HASH).toBe('x-network-context-hash') - }) - }) -}) diff --git a/packages/network-debugger/src/common.ts b/packages/network-debugger/src/common.ts index 86889d4..d07c285 100644 --- a/packages/network-debugger/src/common.ts +++ b/packages/network-debugger/src/common.ts @@ -2,6 +2,8 @@ import { fileURLToPath } from 'url' import { getStackFrames, initiatorStackPipe } from './utils/stack' import { dirname } from 'path' import { generateUUID } from './utils' +import type { AdapterMode, InspectorTargetOptions, NetworkCapability } from './adapters/types' +import type { LegacyMockRule } from './mock' export interface CDPCallFrame { columnNumber: number @@ -21,6 +23,8 @@ export class RequestDetail { } else { this.id = generateUUID() this.responseInfo = {} + this.requestHeaders = {} + this.responseHeaders = {} } } @@ -46,10 +50,6 @@ export class RequestDetail { } } - isHiden() { - return this.isWebSocket() && ['http://127.0.0.1/', 'ws://127.0.0.1/'].includes(this.url!) - } - isWebSocket() { return ( this.requestHeaders?.['Upgrade'] === 'websocket' || @@ -66,6 +66,7 @@ export class RequestDetail { responseData: any responseStatusCode?: number + responseStatusText?: string responseHeaders: any responseInfo: Partial<{ encodedDataLength: number @@ -82,137 +83,83 @@ export class RequestDetail { } } } -export const PORT = Number(process.env.NETWORK_PORT || 5270) -export const SERVER_PORT = Number(process.env.NETWORK_SERVER_PORT || 5271) -export const REMOTE_DEBUGGER_PORT = Number(process.env.REMOTE_DEBUGGER_PORT || 9333) -export const IS_DEV_MODE = process.env.NETWORK_DEBUG_MODE === 'true' -export const READY_MESSAGE = 'ready' - export const __filename = fileURLToPath(import.meta.url) export const __dirname = dirname(__filename) +export interface InterceptOptions { + /** Whether to intercept the global Fetch implementation. */ + fetch?: boolean + /** Whether to intercept `http.request` and `https.request`. */ + normal?: boolean + /** Optional interception for the separately installed `undici` package. */ + undici?: + | false + | { + fetch?: false | true | Record + normal?: false | true | Record + } +} + export interface RegisterOptions { - /** - * @description Main Process Port - * @default 5270 - */ - port?: number - /** - * @description CDP Server Port, used for Devtool - * @link devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${serverPort} - * @default 5271 - */ - serverPort?: number + /** Select the complete capture/backend implementation. Defaults to `auto`. */ + mode?: AdapterMode - /** - * @description Whether to automatically open Devtool - * @default true - */ - autoOpenDevtool?: boolean + /** @deprecated Use `mode`. */ + adapter?: AdapterMode - /** - * @description Options for intercepting different types of requests. - * If a property is set to `false`, that specific type of request will not be intercepted. - * By default, all are intercepted if not explicitly set. - */ - intercept?: { - /** - * @description Whether to intercept `fetch` requests. - * @default true - */ - fetch?: boolean - /** - * @description Whether to intercept `http/https` requests. - * @default true - */ - normal?: boolean - /** - * @description Options for intercepting `undici` requests. - * Set to `false` to disable all undici interception. - * Otherwise, configure specific undici interception options. - * @default false - */ - undici?: - | false - | { - /** - * @description Whether to intercept `undici`'s `fetch` requests. - * @default false - */ - fetch?: false | {} - /** - * @description Whether to intercept `undici`'s normal requests. - * @default false - */ - normal?: false | {} - } + /** Capabilities that the selected backend must provide. */ + requiredCapabilities?: readonly NetworkCapability[] + + /** @deprecated Use `requiredCapabilities`. */ + requiredFeatures?: readonly NetworkCapability[] + + /** Settings used when a Node Inspector target must be created. */ + inspector?: InspectorTargetOptions + + /** Frontend behavior is opt-in and independent from target ownership. */ + devtools?: { + open?: boolean } -} -export const NETWORK_CONTEXT_KEY = 'x-network-context' -export const WS_PROTOCOL = 'ws' - -export const CONTEXT_KEY_PORT = 'x-network-context-port' -export const CONTEXT_KEY_SERVER_PORT = 'x-network-context-server-port' -export const CONTEXT_KEY_AUTO_OPEN_DEVTOOL = 'x-network-context-auto-open-devtools' -export const CONTEXT_KEY_INTERCEPT_NORMAL = 'x-network-context-intercept-normal' -export const CONTEXT_KEY_INTERCEPT_FETCH = 'x-network-context-intercept-fetch' -export const CONTEXT_KEY_INTERCEPT_UNDICI_FETCH = 'x-network-context-intercept-undici-fetch' -export const CONTEXT_KEY_HASH = 'x-network-context-hash' - -export interface ConnectOptions { - /** - * @description Main Process Port - * @default 5270 - */ - port?: number -} -export interface UnregisterOptions { - /** - * @description Main Process Port - * @default 5270 - */ - port?: number -} + /** Persist backend-neutral CDP events and response bodies for later export/replay. */ + session?: { + /** Exact output directory. It must not already contain Session artifacts. */ + directory: string + bodyCommandTimeoutMs?: number + /** Export HAR during disposal. `true` writes `/session.har`. */ + har?: boolean | string + } -export interface SendMessageOptions { - /** - * @description Main Process Port - * @default 5270 - */ - port?: number -} -export type RequestPipe = (req: RequestDetail) => RequestDetail | null + /** Namespaced Legacy settings. Top-level legacy fields remain supported. */ + legacy?: { + /** @deprecated The application bridge now uses child-process IPC. */ + port?: number + /** Legacy CDP target port. Defaults to `0` (an OS-assigned loopback port). */ + serverPort?: number + intercept?: InterceptOptions + /** Deterministic outbound request/response mocks. Legacy backend only. */ + mock?: readonly LegacyMockRule[] + } -export interface SetRequestInterceptorOptions { /** - * @description Main Process Port - * @default 5270 + * @deprecated The application bridge now uses child-process IPC. Move other + * Legacy settings under `legacy`. */ port?: number - request?: RequestPipe -} -export interface SetResponseInterceptorOptions { /** - * @description Main Process Port - * @default 5270 + * @deprecated Use `legacy.serverPort`. Defaults to `0`. */ - port?: number - response?: RequestPipe -} + serverPort?: number -export interface RemoveRequestInterceptorOptions { /** - * @description Main Process Port - * @default 5270 + * @deprecated Use `devtools.open`. Defaults to `false`. */ - port?: number -} + autoOpenDevtool?: boolean -export interface RemoveResponseInterceptorOptions { /** - * @description Main Process Port - * @default 5270 + * @description Options for intercepting different types of requests. + * If a property is set to `false`, that specific type of request will not be intercepted. + * By default, all are intercepted if not explicitly set. */ - port?: number + intercept?: InterceptOptions } diff --git a/packages/network-debugger/src/config/errors.ts b/packages/network-debugger/src/config/errors.ts new file mode 100644 index 0000000..c05aff2 --- /dev/null +++ b/packages/network-debugger/src/config/errors.ts @@ -0,0 +1,17 @@ +export type NndConfigErrorCode = + | 'NND_CONFIG_NOT_FOUND' + | 'NND_CONFIG_LOAD_FAILED' + | 'NND_CONFIG_INVALID' + | 'NND_CONFIG_ENV_INVALID' + +export class NndConfigError extends Error { + constructor( + readonly code: NndConfigErrorCode, + message: string, + readonly details: Readonly> = {}, + readonly cause?: unknown + ) { + super(message) + this.name = 'NndConfigError' + } +} diff --git a/packages/network-debugger/src/config/index.ts b/packages/network-debugger/src/config/index.ts new file mode 100644 index 0000000..b363a5a --- /dev/null +++ b/packages/network-debugger/src/config/index.ts @@ -0,0 +1,11 @@ +export * from './errors' +export * from './loader' +export * from './preload-config' +export * from './types' + +import type { NndConfig } from './types' + +/** Type helper for nnd.config.mjs/cjs files. */ +export function defineConfig(config: T): T { + return config +} diff --git a/packages/network-debugger/src/config/loader.test.ts b/packages/network-debugger/src/config/loader.test.ts new file mode 100644 index 0000000..77136d4 --- /dev/null +++ b/packages/network-debugger/src/config/loader.test.ts @@ -0,0 +1,176 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { NndConfigError } from './errors' +import { findConfigFile, loadConfigFile, resolveConfig } from './loader' + +const temporaryDirectories: string[] = [] + +function temporaryDirectory(): string { + const directory = mkdtempSync(join(tmpdir(), 'nnd-config-')) + temporaryDirectories.push(directory) + return directory +} + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +describe('NND config loader', () => { + it('merges defaults, file, environment, and CLI in documented precedence', async () => { + const cwd = temporaryDirectory() + writeFileSync( + join(cwd, 'nnd.config.json'), + JSON.stringify({ + mode: 'legacy', + open: true, + wait: false, + runner: 'tsx', + inspector: { host: 'file-host', port: 1111 }, + requiredCapabilities: ['requestBody'], + session: { directory: '.nnd/session', har: true }, + legacy: { port: 5000, serverPort: 5001 } + }) + ) + + const result = await resolveConfig({ + cwd, + env: { + NND_MODE: 'native', + NND_OPEN: 'false', + NND_INSPECTOR_PORT: '2222', + NND_WATCH: 'true' + }, + cli: { + mode: 'auto', + wait: true, + inspector: { host: 'cli-host' }, + requiredCapabilities: ['responseBody'] + } + }) + + expect(result.config).toEqual({ + mode: 'auto', + open: false, + wait: true, + watch: true, + runner: 'tsx', + inspector: { host: 'cli-host', port: 2222 }, + requiredCapabilities: ['responseBody'], + session: { directory: '.nnd/session', har: true }, + legacy: { port: 5000, serverPort: 5001 } + }) + expect(result.sources.configFile).toBe(join(cwd, 'nnd.config.json')) + expect(result.sources.env).toEqual(['NND_MODE', 'NND_OPEN', 'NND_WATCH', 'NND_INSPECTOR_PORT']) + expect(result.sources.cli).toEqual(['mode', 'wait', 'requiredCapabilities', 'inspector']) + }) + + it('loads mjs, cjs, and json configuration files', async () => { + const cwd = temporaryDirectory() + const mjs = join(cwd, 'one.mjs') + const cjs = join(cwd, 'two.cjs') + const json = join(cwd, 'three.json') + writeFileSync(mjs, 'export default { mode: "native", open: true }\n') + writeFileSync(cjs, 'module.exports = { runner: "tsx", watch: true }\n') + writeFileSync(json, JSON.stringify({ inspector: { port: 42 } })) + + await expect(loadConfigFile(mjs)).resolves.toMatchObject({ mode: 'native', open: true }) + await expect(loadConfigFile(cjs)).resolves.toMatchObject({ runner: 'tsx', watch: true }) + await expect(loadConfigFile(json)).resolves.toMatchObject({ inspector: { port: 42 } }) + }) + + it('discovers config names deterministically and supports explicit nested paths', async () => { + const cwd = temporaryDirectory() + writeFileSync(join(cwd, 'nnd.config.json'), '{}') + writeFileSync(join(cwd, 'nnd.config.cjs'), 'module.exports = {}\n') + writeFileSync(join(cwd, 'nnd.config.mjs'), 'export default {}\n') + expect(findConfigFile(cwd)).toBe(join(cwd, 'nnd.config.mjs')) + + const nested = join(cwd, 'configs') + mkdirSync(nested) + writeFileSync(join(nested, 'custom.json'), JSON.stringify({ mode: 'legacy' })) + await expect( + resolveConfig({ cwd, env: {}, configFile: 'configs/custom.json' }) + ).resolves.toMatchObject({ config: { mode: 'legacy' } }) + }) + + it('can disable file discovery', async () => { + const cwd = temporaryDirectory() + writeFileSync(join(cwd, 'nnd.config.json'), JSON.stringify({ open: true })) + const result = await resolveConfig({ cwd, env: {}, configFile: false }) + expect(result.config.open).toBe(false) + expect(result.sources.configFile).toBeUndefined() + }) + + it('preserves serializable Legacy mock rules and rejects ambiguous bodies', async () => { + const cwd = temporaryDirectory() + const configPath = join(cwd, 'mock.json') + writeFileSync( + configPath, + JSON.stringify({ + mode: 'auto', + legacy: { + mock: [ + { + match: { url: 'https://example.test/*', method: 'POST' }, + response: { status: 201, bodyBase64: 'AAEC/w==' } + } + ] + } + }) + ) + await expect(resolveConfig({ cwd, env: {}, configFile: configPath })).resolves.toMatchObject({ + config: { + legacy: { + mock: [ + { + match: { url: 'https://example.test/*', method: 'POST' }, + response: { status: 201, bodyBase64: 'AAEC/w==' } + } + ] + } + } + }) + + writeFileSync( + configPath, + JSON.stringify({ + legacy: { + mock: [{ match: { url: '*' }, response: { body: 'text', bodyBase64: 'dGV4dA==' } }] + } + }) + ) + await expect(loadConfigFile(configPath)).rejects.toMatchObject({ + code: 'NND_CONFIG_INVALID' + }) + }) + + it('uses stable actionable errors for missing, invalid, and malformed config', async () => { + const cwd = temporaryDirectory() + await expect(loadConfigFile(join(cwd, 'missing.json'))).rejects.toMatchObject({ + code: 'NND_CONFIG_NOT_FOUND' + }) + + writeFileSync(join(cwd, 'bad.json'), '{ nope') + await expect(loadConfigFile(join(cwd, 'bad.json'))).rejects.toMatchObject({ + code: 'NND_CONFIG_LOAD_FAILED' + }) + + writeFileSync(join(cwd, 'shape.json'), JSON.stringify({ runner: 'deno' })) + await expect(loadConfigFile(join(cwd, 'shape.json'))).rejects.toMatchObject({ + code: 'NND_CONFIG_INVALID' + }) + + await expect( + resolveConfig({ cwd, configFile: false, env: { NND_OPEN: 'maybe' } }) + ).rejects.toBeInstanceOf(NndConfigError) + await expect( + resolveConfig({ cwd, configFile: false, env: { NND_OPEN: 'maybe' } }) + ).rejects.toMatchObject({ + code: 'NND_CONFIG_ENV_INVALID' + }) + }) +}) diff --git a/packages/network-debugger/src/config/loader.ts b/packages/network-debugger/src/config/loader.ts new file mode 100644 index 0000000..65d5412 --- /dev/null +++ b/packages/network-debugger/src/config/loader.ts @@ -0,0 +1,511 @@ +import { existsSync, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { isAbsolute, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { NETWORK_CAPABILITIES, type NetworkCapability } from '../adapters/types' +import { NndConfigError } from './errors' +import { + DEFAULT_NND_CONFIG, + type ConfigResolution, + type NndConfig, + type NndRunner, + type ResolveConfigOptions, + type ResolvedNndConfig +} from './types' + +const CONFIG_NAMES = ['nnd.config.mjs', 'nnd.config.cjs', 'nnd.config.json'] as const +const MODES = new Set(['auto', 'native', 'legacy']) +const RUNNERS = new Set(['node', 'tsx']) +const CAPABILITIES = new Set(NETWORK_CAPABILITIES) + +const hasOwn = (value: object, key: PropertyKey) => Object.prototype.hasOwnProperty.call(value, key) + +function invalid( + message: string, + details: Readonly> = {}, + source: 'config' | 'env' = 'config' +): never { + throw new NndConfigError( + source === 'env' ? 'NND_CONFIG_ENV_INVALID' : 'NND_CONFIG_INVALID', + message, + details + ) +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function assertBoolean(value: unknown, field: string): asserts value is boolean { + if (typeof value !== 'boolean') + invalid(`Configuration field "${field}" must be a boolean.`, { field, value }) +} + +function assertPort(value: unknown, field: string): asserts value is number { + if (!Number.isInteger(value) || (value as number) < 0 || (value as number) > 65_535) { + invalid(`Configuration field "${field}" must be an integer from 0 to 65535.`, { + field, + value + }) + } +} + +function validateMockRules(value: unknown, sourcePath?: string): void { + if (!Array.isArray(value)) { + invalid('Configuration field "legacy.mock" must be an array.', { + field: 'legacy.mock', + value, + sourcePath + }) + } + for (const [index, rule] of value.entries()) { + if ( + !isRecord(rule) || + !isRecord(rule.match) || + typeof rule.match.url !== 'string' || + rule.match.url.length === 0 || + !isRecord(rule.response) + ) { + invalid(`Configuration field "legacy.mock[${index}]" is invalid.`, { + field: `legacy.mock[${index}]`, + value: rule, + sourcePath + }) + } + if (rule.match.method !== undefined && typeof rule.match.method !== 'string') { + invalid(`Configuration field "legacy.mock[${index}].match.method" must be a string.`, { + field: `legacy.mock[${index}].match.method`, + value: rule.match.method, + sourcePath + }) + } + if (rule.match.headers !== undefined && !isRecord(rule.match.headers)) { + invalid(`Configuration field "legacy.mock[${index}].match.headers" must be an object.`, { + field: `legacy.mock[${index}].match.headers`, + value: rule.match.headers, + sourcePath + }) + } + if (rule.response.body !== undefined && typeof rule.response.body !== 'string') { + invalid(`Configuration field "legacy.mock[${index}].response.body" must be a string.`, { + field: `legacy.mock[${index}].response.body`, + value: rule.response.body, + sourcePath + }) + } + if (rule.response.bodyBase64 !== undefined && typeof rule.response.bodyBase64 !== 'string') { + invalid(`Configuration field "legacy.mock[${index}].response.bodyBase64" must be a string.`, { + field: `legacy.mock[${index}].response.bodyBase64`, + value: rule.response.bodyBase64, + sourcePath + }) + } + if (rule.response.body !== undefined && rule.response.bodyBase64 !== undefined) { + invalid( + `Configuration field "legacy.mock[${index}].response" cannot define both body and bodyBase64.`, + { field: `legacy.mock[${index}].response`, sourcePath } + ) + } + } +} + +function validateConfig(value: unknown, sourcePath?: string): NndConfig { + if (!isRecord(value)) { + return invalid('NND configuration must export an object.', { sourcePath }) + } + + const config = value as Record + if (config.mode !== undefined && (typeof config.mode !== 'string' || !MODES.has(config.mode))) { + invalid('Configuration field "mode" must be auto, native, or legacy.', { + field: 'mode', + value: config.mode, + sourcePath + }) + } + if (config.runner !== undefined && !RUNNERS.has(config.runner as NndRunner)) { + invalid('Configuration field "runner" must be node or tsx.', { + field: 'runner', + value: config.runner, + sourcePath + }) + } + for (const field of ['open', 'wait', 'watch'] as const) { + if (config[field] !== undefined) assertBoolean(config[field], field) + } + + if (config.inspector !== undefined) { + if (!isRecord(config.inspector)) { + invalid('Configuration field "inspector" must be an object.', { + field: 'inspector', + value: config.inspector, + sourcePath + }) + } + if (config.inspector.host !== undefined && typeof config.inspector.host !== 'string') { + invalid('Configuration field "inspector.host" must be a string.', { + field: 'inspector.host', + value: config.inspector.host, + sourcePath + }) + } + if (config.inspector.host === '') { + invalid('Configuration field "inspector.host" cannot be empty.', { + field: 'inspector.host', + sourcePath + }) + } + if (config.inspector.port !== undefined) assertPort(config.inspector.port, 'inspector.port') + } + + if (config.requiredCapabilities !== undefined) { + if ( + !Array.isArray(config.requiredCapabilities) || + config.requiredCapabilities.some( + (capability) => typeof capability !== 'string' || !CAPABILITIES.has(capability) + ) + ) { + invalid('Configuration field "requiredCapabilities" contains an unknown capability.', { + field: 'requiredCapabilities', + value: config.requiredCapabilities, + allowed: [...NETWORK_CAPABILITIES], + sourcePath + }) + } + } + + if (config.session !== undefined) { + if (!isRecord(config.session)) { + invalid('Configuration field "session" must be an object.', { + field: 'session', + value: config.session, + sourcePath + }) + } + if (typeof config.session.directory !== 'string' || !config.session.directory.trim()) { + invalid('Configuration field "session.directory" must be a non-empty string.', { + field: 'session.directory', + value: config.session.directory, + sourcePath + }) + } + if ( + config.session.bodyCommandTimeoutMs !== undefined && + (!Number.isSafeInteger(config.session.bodyCommandTimeoutMs) || + (config.session.bodyCommandTimeoutMs as number) <= 0) + ) { + invalid('Configuration field "session.bodyCommandTimeoutMs" must be a positive integer.', { + field: 'session.bodyCommandTimeoutMs', + value: config.session.bodyCommandTimeoutMs, + sourcePath + }) + } + if ( + config.session.har !== undefined && + typeof config.session.har !== 'boolean' && + (typeof config.session.har !== 'string' || !config.session.har.trim()) + ) { + invalid('Configuration field "session.har" must be a boolean or non-empty path.', { + field: 'session.har', + value: config.session.har, + sourcePath + }) + } + } + + if (config.legacy !== undefined) { + if (!isRecord(config.legacy)) { + invalid('Configuration field "legacy" must be an object.', { + field: 'legacy', + value: config.legacy, + sourcePath + }) + } + if (config.legacy.port !== undefined) assertPort(config.legacy.port, 'legacy.port') + if (config.legacy.serverPort !== undefined) { + assertPort(config.legacy.serverPort, 'legacy.serverPort') + } + if (config.legacy.intercept !== undefined && !isRecord(config.legacy.intercept)) { + invalid('Configuration field "legacy.intercept" must be an object.', { + field: 'legacy.intercept', + value: config.legacy.intercept, + sourcePath + }) + } + if (config.legacy.mock !== undefined) { + validateMockRules(config.legacy.mock, sourcePath) + } + } + + return config as unknown as NndConfig +} + +function resolveConfigPath(cwd: string, requested: string): string { + return isAbsolute(requested) ? requested : resolve(cwd, requested) +} + +export function findConfigFile(cwd: string): string | undefined { + for (const name of CONFIG_NAMES) { + const candidate = resolve(cwd, name) + if (existsSync(candidate)) return candidate + } + return undefined +} + +export async function loadConfigFile(path: string): Promise { + if (!existsSync(path)) { + throw new NndConfigError( + 'NND_CONFIG_NOT_FOUND', + `NND configuration file was not found: ${path}`, + { + path + } + ) + } + + try { + let value: unknown + if (path.endsWith('.json')) { + value = JSON.parse(readFileSync(path, 'utf8')) + } else if (path.endsWith('.cjs')) { + const requireFromConfig = createRequire(resolve(path, '..', '__nnd_loader__.cjs')) + const resolved = requireFromConfig.resolve(path) + delete requireFromConfig.cache[resolved] + const loaded = requireFromConfig(resolved) as unknown + value = isRecord(loaded) && hasOwn(loaded, 'default') ? loaded.default : loaded + } else if (path.endsWith('.mjs')) { + const url = `${pathToFileURL(path).href}?nnd=${Date.now()}` + const loaded = (await import(/* @vite-ignore */ url)) as { default?: unknown } + value = hasOwn(loaded, 'default') ? loaded.default : loaded + } else { + invalid('NND configuration files must use .mjs, .cjs, or .json.', { path }) + } + return validateConfig(value, path) + } catch (error) { + if (error instanceof NndConfigError) throw error + throw new NndConfigError( + 'NND_CONFIG_LOAD_FAILED', + `Unable to load NND configuration: ${path}`, + { path, cause: error instanceof Error ? error.message : String(error) }, + error + ) + } +} + +function parseBoolean(value: string, key: string): boolean { + const normalized = value.trim().toLowerCase() + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true + if (['0', 'false', 'no', 'off'].includes(normalized)) return false + return invalid(`Environment variable ${key} must be true or false.`, { key, value }, 'env') +} + +function parsePort(value: string, key: string): number { + const port = Number(value) + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + return invalid( + `Environment variable ${key} must be an integer from 0 to 65535.`, + { + key, + value + }, + 'env' + ) + } + return port +} + +function parseCapabilities(value: string, key: string): readonly NetworkCapability[] { + const capabilities = value + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + const unknown = capabilities.filter((capability) => !CAPABILITIES.has(capability)) + if (unknown.length > 0) { + return invalid( + `Environment variable ${key} contains unknown capabilities: ${unknown.join(', ')}.`, + { + key, + unknown, + allowed: [...NETWORK_CAPABILITIES] + }, + 'env' + ) + } + return [...new Set(capabilities)] as NetworkCapability[] +} + +interface EnvironmentConfig { + config: NndConfig + keys: string[] +} + +function configFromEnvironment(env: NodeJS.ProcessEnv): EnvironmentConfig { + const config: NndConfig = {} + const keys: string[] = [] + const use = (key: string, apply: (value: string) => void) => { + const value = env[key] + if (value === undefined || value === '') return + keys.push(key) + apply(value) + } + + use('NND_MODE', (value) => { + if (!MODES.has(value)) { + invalid( + 'Environment variable NND_MODE must be auto, native, or legacy.', + { + key: 'NND_MODE', + value + }, + 'env' + ) + } + config.mode = value as NndConfig['mode'] + }) + use('NND_OPEN', (value) => (config.open = parseBoolean(value, 'NND_OPEN'))) + use('NND_WAIT', (value) => (config.wait = parseBoolean(value, 'NND_WAIT'))) + use('NND_WATCH', (value) => (config.watch = parseBoolean(value, 'NND_WATCH'))) + use('NND_RUNNER', (value) => { + if (!RUNNERS.has(value as NndRunner)) { + invalid( + 'Environment variable NND_RUNNER must be node or tsx.', + { + key: 'NND_RUNNER', + value + }, + 'env' + ) + } + config.runner = value as NndRunner + }) + use('NND_INSPECTOR_HOST', (value) => { + if (!value.trim()) { + invalid( + 'Environment variable NND_INSPECTOR_HOST cannot be empty.', + { + key: 'NND_INSPECTOR_HOST' + }, + 'env' + ) + } + config.inspector = { ...config.inspector, host: value } + }) + use('NND_INSPECTOR_PORT', (value) => { + config.inspector = { ...config.inspector, port: parsePort(value, 'NND_INSPECTOR_PORT') } + }) + use('NND_REQUIRED_CAPABILITIES', (value) => { + config.requiredCapabilities = parseCapabilities(value, 'NND_REQUIRED_CAPABILITIES') + }) + use('NND_LEGACY_PORT', (value) => { + config.legacy = { ...config.legacy, port: parsePort(value, 'NND_LEGACY_PORT') } + }) + use('NND_LEGACY_SERVER_PORT', (value) => { + config.legacy = { + ...config.legacy, + serverPort: parsePort(value, 'NND_LEGACY_SERVER_PORT') + } + }) + + return { config, keys } +} + +function mergeConfig(base: NndConfig, override: NndConfig): NndConfig { + return { + ...base, + ...override, + inspector: + base.inspector || override.inspector + ? { ...base.inspector, ...override.inspector } + : undefined, + session: override.session ?? base.session, + legacy: + base.legacy || override.legacy + ? { + ...base.legacy, + ...override.legacy, + intercept: + base.legacy?.intercept || override.legacy?.intercept + ? { ...base.legacy?.intercept, ...override.legacy?.intercept } + : undefined, + mock: override.legacy?.mock ?? base.legacy?.mock + } + : undefined + } +} + +function explicitKeys(config: NndConfig): string[] { + const keys: string[] = [] + for (const key of [ + 'mode', + 'open', + 'wait', + 'watch', + 'runner', + 'requiredCapabilities', + 'session' + ] as const) { + if (config[key] !== undefined) keys.push(key) + } + if (config.inspector !== undefined) keys.push('inspector') + if (config.legacy !== undefined) keys.push('legacy') + return keys +} + +function finalize(config: NndConfig): ResolvedNndConfig { + const merged = mergeConfig(DEFAULT_NND_CONFIG, config) + return { + mode: merged.mode!, + open: merged.open!, + wait: merged.wait!, + watch: merged.watch!, + runner: merged.runner!, + inspector: { + host: merged.inspector!.host!, + port: merged.inspector!.port! + }, + requiredCapabilities: [...(merged.requiredCapabilities ?? [])], + ...(merged.session + ? { + session: { + directory: merged.session.directory, + ...(merged.session.bodyCommandTimeoutMs !== undefined + ? { bodyCommandTimeoutMs: merged.session.bodyCommandTimeoutMs } + : {}), + ...(merged.session.har !== undefined ? { har: merged.session.har } : {}) + } + } + : {}), + legacy: { + ...(merged.legacy?.port !== undefined ? { port: merged.legacy.port } : {}), + ...(merged.legacy?.serverPort !== undefined ? { serverPort: merged.legacy.serverPort } : {}), + ...(merged.legacy?.intercept !== undefined + ? { intercept: { ...merged.legacy.intercept } } + : {}), + ...(merged.legacy?.mock !== undefined ? { mock: [...merged.legacy.mock] } : {}) + } + } +} + +export async function resolveConfig(options: ResolveConfigOptions = {}): Promise { + const cwd = resolve(options.cwd ?? process.cwd()) + const env = options.env ?? process.env + const requestedConfig = options.configFile ?? env.NND_CONFIG + const configPath = + requestedConfig === false + ? undefined + : typeof requestedConfig === 'string' && requestedConfig !== '' + ? resolveConfigPath(cwd, requestedConfig) + : findConfigFile(cwd) + const fileConfig = configPath ? await loadConfigFile(configPath) : {} + const environment = configFromEnvironment(env) + const cliConfig = options.cli ? validateConfig(options.cli) : {} + const config = finalize(mergeConfig(mergeConfig(fileConfig, environment.config), cliConfig)) + + return { + config, + sources: { + ...(configPath ? { configFile: configPath } : {}), + env: environment.keys, + cli: explicitKeys(cliConfig) + } + } +} diff --git a/packages/network-debugger/src/config/preload-config.test.ts b/packages/network-debugger/src/config/preload-config.test.ts new file mode 100644 index 0000000..5e74178 --- /dev/null +++ b/packages/network-debugger/src/config/preload-config.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { parsePreloadConfig, serializePreloadConfig, toRegisterOptions } from './preload-config' +import type { ResolvedNndConfig } from './types' + +const config: ResolvedNndConfig = { + mode: 'native', + open: true, + wait: false, + watch: true, + runner: 'tsx', + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: ['responseBody'], + session: { directory: '.nnd/session', bodyCommandTimeoutMs: 2500, har: true }, + legacy: { port: 5000 } +} + +describe('preload configuration transport', () => { + it('round trips resolved config and excludes CLI-owned frontend opening', () => { + expect(parsePreloadConfig(serializePreloadConfig(config))).toEqual(config) + expect(toRegisterOptions(config)).toEqual({ + mode: 'native', + requiredCapabilities: ['responseBody'], + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false }, + session: { directory: '.nnd/session', bodyCommandTimeoutMs: 2500, har: true }, + legacy: { port: 5000 } + }) + }) + + it('reports malformed serialized environment values with a stable code', () => { + expect(() => parsePreloadConfig('{')).toThrowError( + expect.objectContaining({ code: 'NND_CONFIG_ENV_INVALID' }) + ) + expect(() => parsePreloadConfig(JSON.stringify({ mode: 'wat' }))).toThrowError( + expect.objectContaining({ code: 'NND_CONFIG_ENV_INVALID' }) + ) + expect(() => parsePreloadConfig(JSON.stringify({ ...config, session: {} }))).toThrowError( + expect.objectContaining({ code: 'NND_CONFIG_ENV_INVALID' }) + ) + }) +}) diff --git a/packages/network-debugger/src/config/preload-config.ts b/packages/network-debugger/src/config/preload-config.ts new file mode 100644 index 0000000..12cec7b --- /dev/null +++ b/packages/network-debugger/src/config/preload-config.ts @@ -0,0 +1,60 @@ +import type { RegisterOptions } from '../common' +import { NndConfigError } from './errors' +import type { ResolvedNndConfig } from './types' + +export const NND_PRELOAD_CONFIG_ENV = 'NND_PRELOAD_CONFIG' + +export function toRegisterOptions(config: ResolvedNndConfig): RegisterOptions { + return { + mode: config.mode, + requiredCapabilities: config.requiredCapabilities, + inspector: config.inspector, + devtools: { open: false }, + ...(config.session ? { session: config.session } : {}), + legacy: config.legacy + } +} + +export function serializePreloadConfig(config: ResolvedNndConfig): string { + return JSON.stringify(config) +} + +export function parsePreloadConfig(value: string): ResolvedNndConfig { + try { + const parsed = JSON.parse(value) as ResolvedNndConfig + if ( + !parsed || + typeof parsed !== 'object' || + !['auto', 'native', 'legacy'].includes(parsed.mode) || + typeof parsed.open !== 'boolean' || + typeof parsed.wait !== 'boolean' || + typeof parsed.watch !== 'boolean' || + !['node', 'tsx'].includes(parsed.runner) || + !parsed.inspector || + typeof parsed.inspector.host !== 'string' || + !Number.isInteger(parsed.inspector.port) || + !Array.isArray(parsed.requiredCapabilities) || + (parsed.session !== undefined && + (!parsed.session || + typeof parsed.session !== 'object' || + typeof parsed.session.directory !== 'string' || + !parsed.session.directory.trim() || + (parsed.session.bodyCommandTimeoutMs !== undefined && + (!Number.isSafeInteger(parsed.session.bodyCommandTimeoutMs) || + parsed.session.bodyCommandTimeoutMs <= 0)) || + (parsed.session.har !== undefined && + typeof parsed.session.har !== 'boolean' && + (typeof parsed.session.har !== 'string' || !parsed.session.har.trim())))) + ) { + throw new Error('serialized configuration has an invalid shape') + } + return parsed + } catch (error) { + throw new NndConfigError( + 'NND_CONFIG_ENV_INVALID', + `${NND_PRELOAD_CONFIG_ENV} does not contain a valid resolved configuration.`, + { cause: error instanceof Error ? error.message : String(error) }, + error + ) + } +} diff --git a/packages/network-debugger/src/config/types.ts b/packages/network-debugger/src/config/types.ts new file mode 100644 index 0000000..31d3890 --- /dev/null +++ b/packages/network-debugger/src/config/types.ts @@ -0,0 +1,84 @@ +import type { InterceptOptions } from '../common' +import type { AdapterMode, NetworkCapability } from '../adapters/types' +import type { LegacyMockRule } from '../mock' + +export type NndRunner = 'node' | 'tsx' + +/** Values accepted by nnd.config.mjs/cjs/json and explicit CLI overrides. */ +export interface NndConfig { + mode?: AdapterMode + open?: boolean + wait?: boolean + watch?: boolean + runner?: NndRunner + inspector?: { + host?: string + port?: number + } + requiredCapabilities?: readonly NetworkCapability[] + session?: { + directory: string + bodyCommandTimeoutMs?: number + har?: boolean | string + } + legacy?: { + port?: number + serverPort?: number + intercept?: InterceptOptions + mock?: readonly LegacyMockRule[] + } +} + +export interface ResolvedNndConfig { + mode: AdapterMode + open: boolean + wait: boolean + watch: boolean + runner: NndRunner + inspector: { + host: string + port: number + } + requiredCapabilities: readonly NetworkCapability[] + session?: { + directory: string + bodyCommandTimeoutMs?: number + har?: boolean | string + } + legacy: { + port?: number + serverPort?: number + intercept?: InterceptOptions + mock?: readonly LegacyMockRule[] + } +} + +export interface ConfigSources { + configFile?: string + env: readonly string[] + cli: readonly string[] +} + +export interface ConfigResolution { + config: ResolvedNndConfig + sources: ConfigSources +} + +export interface ResolveConfigOptions { + cwd?: string + env?: NodeJS.ProcessEnv + cli?: NndConfig + /** Explicit file path. `false` disables config-file discovery. */ + configFile?: string | false +} + +export const DEFAULT_NND_CONFIG: Readonly = Object.freeze({ + mode: 'auto', + open: false, + wait: true, + watch: false, + runner: 'node', + inspector: Object.freeze({ host: '127.0.0.1', port: 0 }), + requiredCapabilities: Object.freeze([]), + legacy: Object.freeze({}) +}) diff --git a/packages/network-debugger/src/core/capture-scope.ts b/packages/network-debugger/src/core/capture-scope.ts new file mode 100644 index 0000000..798181b --- /dev/null +++ b/packages/network-debugger/src/core/capture-scope.ts @@ -0,0 +1,12 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +const legacyCaptureSuppression = new AsyncLocalStorage() + +/** Run internal debugger transport setup without observing it as application traffic. */ +export function withoutLegacyCapture(operation: () => T): T { + return legacyCaptureSuppression.run(true, operation) +} + +export function isLegacyCaptureSuppressed(): boolean { + return legacyCaptureSuppression.getStore() === true +} diff --git a/packages/network-debugger/src/core/fetch.test.ts b/packages/network-debugger/src/core/fetch.test.ts index 5b0e936..e4b1b04 100644 --- a/packages/network-debugger/src/core/fetch.test.ts +++ b/packages/network-debugger/src/core/fetch.test.ts @@ -1,769 +1,282 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import { RequestDetail } from '../common' -import { proxyFetch, fetchProxyFactory } from './fetch' +import { deserialize, serialize } from 'node:v8' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import type { MainProcess } from './fork' +import { fetchProxyFactory, proxyFetch, SseParser } from './fetch' import * as cellModule from './hooks/cell' -// Mock setCurrentCell +const cellState = vi.hoisted(() => ({ current: null as unknown })) + vi.mock('./hooks/cell', () => ({ - setCurrentCell: vi.fn(), - getCurrentCell: vi.fn() + setCurrentCell: vi.fn((cell: unknown) => { + cellState.current = cell + }), + getCurrentCell: vi.fn(() => cellState.current) })) -// 创建 mock MainProcess -function createMockMainProcess() { - const mockSendRequest = vi.fn() - const mockMainProcess = { - sendRequest: mockSendRequest.mockReturnThis() - } - return { mockMainProcess, mockSendRequest } +interface JournalEntry { + transport: 'request' | 'event' + type: string + data: any } -// 创建 mock Response -function createMockResponse( - options: { - status?: number - headers?: Record - body?: string | Buffer - } = {} -) { - const { status = 200, headers = {}, body = '' } = options - const mockHeaders = new Headers(headers) - - const arrayBuffer = typeof body === 'string' ? new TextEncoder().encode(body).buffer : body.buffer - - return { - status, - headers: mockHeaders, - clone: vi.fn().mockReturnValue({ - arrayBuffer: vi.fn().mockResolvedValue(arrayBuffer) - }) - } as unknown as Response +function snapshot(value: T): T { + return deserialize(serialize(value)) as T } -describe('core/fetch.ts', () => { - let originalFetch: typeof globalThis.fetch | undefined - - beforeEach(() => { - vi.clearAllMocks() - originalFetch = globalThis.fetch +function createMainProcess() { + const journal: JournalEntry[] = [] + const mainProcess: Record = {} + mainProcess.sendRequest = vi.fn((type: string, data: unknown) => { + journal.push({ transport: 'request', type, data: snapshot(data) }) + return mainProcess }) - - afterEach(() => { - // 恢复原始 fetch - if (originalFetch !== undefined) { - globalThis.fetch = originalFetch - } + mainProcess.send = vi.fn(async (event: { type: string; data: unknown }) => { + journal.push({ transport: 'event', type: event.type, data: snapshot(event.data) }) }) + return { journal, mainProcess: mainProcess as MainProcess } +} - describe('proxyFetch 函数', () => { - test('当 globalThis.fetch 不存在时,直接返回 undefined', () => { - // 临时删除 fetch - const savedFetch = globalThis.fetch - // @ts-expect-error - 测试 fetch 不存在的情况 - delete globalThis.fetch - - const { mockMainProcess } = createMockMainProcess() - const result = proxyFetch(mockMainProcess as never) - - expect(result).toBeUndefined() - - // 恢复 - globalThis.fetch = savedFetch - }) - - test('当 globalThis.fetch 存在时,替换为代理函数', () => { - const mockFetch = vi.fn() - globalThis.fetch = mockFetch - - const { mockMainProcess } = createMockMainProcess() - const unset = proxyFetch(mockMainProcess as never) - - expect(globalThis.fetch).not.toBe(mockFetch) - expect(typeof unset).toBe('function') - }) +function fetchMock(response: Response): typeof fetch { + return vi.fn().mockResolvedValue(response) as unknown as typeof fetch +} - test('调用返回的 unset 函数后恢复原始 fetch', () => { - const mockFetch = vi.fn() - globalThis.fetch = mockFetch +function streamResponse(chunks: string[]): Response { + const encoder = new TextEncoder() + const body = new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)) + controller.close() + } + }) + return new Response(body, { + status: 200, + headers: { 'content-type': 'text/event-stream; charset=utf-8' } + }) +} - const { mockMainProcess } = createMockMainProcess() - const unset = proxyFetch(mockMainProcess as never) +async function waitFor(journal: JournalEntry[], type: string): Promise { + await vi.waitFor(() => expect(journal.some((entry) => entry.type === type)).toBe(true)) + return journal.find((entry) => entry.type === type)! +} - expect(globalThis.fetch).not.toBe(mockFetch) +describe('fetch capture', () => { + let savedFetch: typeof globalThis.fetch | undefined - // 调用 unset - unset!() + beforeEach(() => { + vi.clearAllMocks() + cellState.current = null + savedFetch = globalThis.fetch + }) - expect(globalThis.fetch).toBe(mockFetch) - }) + afterEach(() => { + if (savedFetch) globalThis.fetch = savedFetch + else Reflect.deleteProperty(globalThis, 'fetch') }) - describe('fetchProxyFactory 函数', () => { - describe('请求 URL 处理', () => { - test('处理字符串 URL', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - // 验证 sendRequest 被调用,且 URL 正确 - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'https://example.com/api' - }) - ) - }) - - test('处理 URL 对象', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const url = new URL('https://example.com/api') - await proxyFn(url) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'https://example.com/api' - }) - ) - }) - - test('处理 Request 对象(URL 不会被提取)', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - // 注意:Request 对象的 URL 不会被提取到 requestDetail.url - // 因为代码只检查 string 和 URL 类型 - const request = new Request('https://example.com/api') - await proxyFn(request) - - // URL 应该是 undefined,因为 Request 类型不被处理 - // RequestDetail 初始化时 url 属性未定义 - const initRequestCall = mockSendRequest.mock.calls.find((call) => call[0] === 'initRequest') - expect(initRequestCall).toBeDefined() - const requestDetail = initRequestCall![1] as RequestDetail - expect(requestDetail.url).toBeUndefined() - }) - }) + test('installs and unsets the global proxy without overwriting a later owner', () => { + const original = vi.fn() as unknown as typeof fetch + globalThis.fetch = original + const { mainProcess } = createMainProcess() + + const unset = proxyFetch(mainProcess)! + expect(globalThis.fetch).not.toBe(original) + unset() + expect(globalThis.fetch).toBe(original) + + const unsetAgain = proxyFetch(mainProcess)! + const replacement = vi.fn() as unknown as typeof fetch + globalThis.fetch = replacement + unsetAgain() + expect(globalThis.fetch).toBe(replacement) + }) - describe('请求方法处理', () => { - test('默认方法为 GET', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - method: 'GET' - }) - ) - }) - - test('使用 options 中指定的方法', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api', { method: 'POST' }) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - method: 'POST' - }) - ) - }) - }) + test('does nothing when the runtime has no global fetch', () => { + Reflect.deleteProperty(globalThis, 'fetch') + const { mainProcess } = createMainProcess() + expect(proxyFetch(mainProcess)).toBeUndefined() + }) - describe('请求头处理', () => { - test('处理 Headers 对象', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const headers = new Headers({ - 'Content-Type': 'application/json', - Authorization: 'Bearer token' - }) - await proxyFn('https://example.com/api', { headers }) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - requestHeaders: { - 'content-type': 'application/json', - authorization: 'Bearer token' - } - }) - ) - }) - - test('处理普通对象头部', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const headers = { - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value' - } - await proxyFn('https://example.com/api', { headers }) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - requestHeaders: headers - }) - ) - }) - - test('没有头部时使用空对象', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - requestHeaders: {} - }) - ) - }) + test('captures a Request URL, merged headers, body, and a seconds timestamp', async () => { + const response = new Response(null, { status: 204 }) + const original = fetchMock(response) + const { journal, mainProcess } = createMainProcess() + const request = new Request('http://127.0.0.1:43871/actual?value=1', { + method: 'PUT', + headers: { 'X-From-Request': 'base' } }) + const before = Date.now() / 1000 - describe('请求体处理', () => { - test('记录请求体数据', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const body = JSON.stringify({ key: 'value' }) - await proxyFn('https://example.com/api', { method: 'POST', body }) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - requestData: body - }) - ) - }) + await fetchProxyFactory(original, mainProcess)(request, { + method: 'PATCH', + headers: { 'X-From-Options': 'override' }, + body: 'payload' }) - - describe('setCurrentCell 调用', () => { - test('请求开始时设置 cell', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - expect(cellModule.setCurrentCell).toHaveBeenCalledWith( - expect.objectContaining({ - request: expect.any(RequestDetail), - pipes: [], - isAborted: false - }) - ) - }) - - test('请求完成后清除 cell', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - // 最后一次调用应该是 null - const calls = vi.mocked(cellModule.setCurrentCell).mock.calls - expect(calls[calls.length - 1][0]).toBeNull() - }) + const after = Date.now() / 1000 + + const detail = journal.find((entry) => entry.type === 'initRequest')!.data + expect(detail).toMatchObject({ + url: 'http://127.0.0.1:43871/actual?value=1', + method: 'PATCH', + requestData: 'payload', + requestHeaders: { + 'x-from-request': 'base', + 'x-from-options': 'override' + } }) + expect(detail.requestStartTime).toBeGreaterThanOrEqual(before) + expect(detail.requestStartTime).toBeLessThanOrEqual(after) + expect(detail.requestStartTime).toBeLessThan(10_000_000_000) + expect(original).toHaveBeenCalledWith(request, expect.objectContaining({ body: 'payload' })) + }) - describe('MainProcess 消息发送', () => { - test('发送 initRequest 和 registerRequest', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - expect(mockSendRequest).toHaveBeenCalledWith('initRequest', expect.any(RequestDetail)) - expect(mockSendRequest).toHaveBeenCalledWith('registerRequest', expect.any(RequestDetail)) - }) + test('emits responseReceived before the terminal body event', async () => { + const response = new Response('captured body', { + status: 201, + statusText: 'Created', + headers: { 'content-type': 'text/plain', 'x-result': 'yes' } }) - - describe('成功响应处理', () => { - test('记录响应状态码', async () => { - const mockResponse = createMockResponse({ status: 201 }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - // 等待异步操作完成 - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseStatusCode: 201 - }) - ) - }) - - test('记录响应头', async () => { - const mockResponse = createMockResponse({ - headers: { 'Content-Type': 'application/json' } - }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseHeaders: { 'content-type': 'application/json' } - }) - ) - }) - - test('记录响应体数据', async () => { - const responseBody = 'Hello, World!' - const mockResponse = createMockResponse({ body: responseBody }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseData: expect.any(Buffer) - }) - ) - }) - - test('记录响应数据长度', async () => { - const responseBody = 'Hello, World!' - const mockResponse = createMockResponse({ body: responseBody }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseInfo: expect.objectContaining({ - dataLength: responseBody.length, - encodedDataLength: responseBody.length - }) - }) - ) - }) - - test('发送 updateRequest 和 endRequest', async () => { - const mockResponse = createMockResponse() - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith('updateRequest', expect.any(RequestDetail)) - expect(mockSendRequest).toHaveBeenCalledWith('endRequest', expect.any(RequestDetail)) - }) - - test('返回原始 Response 对象', async () => { - const mockResponse = createMockResponse() - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const result = await proxyFn('https://example.com/api') - - expect(result).toBe(mockResponse) - }) - - test('响应状态码为 0 时正确处理', async () => { - const mockResponse = createMockResponse({ status: 0 }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseStatusCode: 0 - }) - ) - }) + const { journal, mainProcess } = createMainProcess() + + const returned = await fetchProxyFactory( + fetchMock(response), + mainProcess + )('https://example.test/resource') + const terminal = await waitFor(journal, 'endRequest') + + expect(returned).toBe(response) + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'responseReceived', + 'endRequest' + ]) + const received = journal.find((entry) => entry.type === 'responseReceived')!.data + expect(received).toMatchObject({ + responseStatusCode: 201, + responseStatusText: 'Created', + responseHeaders: { 'content-type': 'text/plain', 'x-result': 'yes' } }) + expect(Buffer.from(terminal.data.responseData).toString()).toBe('captured body') + expect(terminal.data.responseInfo).toEqual({ dataLength: 13, encodedDataLength: 13 }) + expect(terminal.data.requestEndTime).toBeLessThan(10_000_000_000) + expect(cellState.current).toBeNull() + }) - describe('错误响应处理', () => { - test('请求失败时记录状态码为 0', async () => { - const error = new Error('Network error') - const mockFetch = vi.fn().mockRejectedValue(error) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - - await expect(proxyFn('https://example.com/api')).rejects.toThrow('Network error') - - expect(mockSendRequest).toHaveBeenCalledWith( - 'updateRequest', - expect.objectContaining({ - responseStatusCode: 0 - }) - ) - }) - - test('请求失败时发送 updateRequest 和 endRequest', async () => { - const error = new Error('Network error') - const mockFetch = vi.fn().mockRejectedValue(error) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - - await expect(proxyFn('https://example.com/api')).rejects.toThrow() - - expect(mockSendRequest).toHaveBeenCalledWith('updateRequest', expect.any(RequestDetail)) - expect(mockSendRequest).toHaveBeenCalledWith('endRequest', expect.any(RequestDetail)) - }) - - test('请求失败时重新抛出错误', async () => { - const error = new Error('Custom error message') - const mockFetch = vi.fn().mockRejectedValue(error) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) + test('reports a rejected fetch only as requestFailed and rethrows it', async () => { + const failure = new Error('connection refused') + const original = vi.fn().mockRejectedValue(failure) as unknown as typeof fetch + const { journal, mainProcess } = createMainProcess() + + await expect( + fetchProxyFactory(original, mainProcess)('http://127.0.0.1:49999/unavailable') + ).rejects.toBe(failure) + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'requestFailed' + ]) + const failed = journal.at(-1)!.data + expect(failed.errorText).toBe('connection refused') + expect(failed.request.requestEndTime).toBeLessThan(10_000_000_000) + expect(cellState.current).toBeNull() + }) - await expect(proxyFn('https://example.com/api')).rejects.toThrow('Custom error message') - }) + test('reports body-capture failure after headers without a successful terminal event', async () => { + const response = { + status: 200, + statusText: 'OK', + headers: new Headers({ 'content-type': 'application/octet-stream' }), + clone: () => ({ arrayBuffer: () => Promise.reject(new Error('stream reset')) }) + } as unknown as Response + const { journal, mainProcess } = createMainProcess() + + await fetchProxyFactory(fetchMock(response), mainProcess)('https://example.test/reset') + await waitFor(journal, 'requestFailed') + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'responseReceived', + 'requestFailed' + ]) + expect(journal.some(({ type }) => type === 'endRequest')).toBe(false) + expect(journal.at(-1)!.data).toMatchObject({ errorText: 'stream reset', canceled: true }) + }) - test('请求失败后清除 cell', async () => { - const error = new Error('Network error') - const mockFetch = vi.fn().mockRejectedValue(error) - const { mockMainProcess } = createMockMainProcess() + test('SseParser handles split CRLF delimiters, multiline data, and persistent ids', () => { + const messages: Array<{ eventName: string; eventId: string; data: string }> = [] + const parser = new SseParser((message) => messages.push(message)) - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) + parser.push('id: 7\r') + parser.push('\nevent: update\r\ndata: first\r') + parser.push('\ndata: second\r\n\r') + parser.push('\n: ignored\r\ndata: tail') + parser.finish() - await expect(proxyFn('https://example.com/api')).rejects.toThrow() + expect(messages).toEqual([ + { eventName: 'update', eventId: '7', data: 'first\nsecond' }, + { eventName: 'message', eventId: '7', data: 'tail' } + ]) + }) - // 最后一次调用应该是 null - const calls = vi.mocked(cellModule.setCurrentCell).mock.calls - expect(calls[calls.length - 1][0]).toBeNull() - }) - }) + test('SseParser flushes an unterminated final event and ignores events without data', () => { + const messages: Array<{ eventName: string; eventId: string; data: string }> = [] + const parser = new SseParser((message) => messages.push(message)) - describe('时间戳记录', () => { - test('记录请求开始时间', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const beforeTime = Date.now() - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - const afterTime = Date.now() - - const initRequestCall = mockSendRequest.mock.calls.find((call) => call[0] === 'initRequest') - expect(initRequestCall).toBeDefined() - const requestDetail = initRequestCall![1] as RequestDetail - expect(requestDetail.requestStartTime).toBeGreaterThanOrEqual(beforeTime) - expect(requestDetail.requestStartTime).toBeLessThanOrEqual(afterTime) - }) - - test('记录请求结束时间', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 10)) - - const updateRequestCall = mockSendRequest.mock.calls.find( - (call) => call[0] === 'updateRequest' - ) - expect(updateRequestCall).toBeDefined() - const requestDetail = updateRequestCall![1] as RequestDetail - expect(requestDetail.requestEndTime).toBeDefined() - expect(requestDetail.requestEndTime).toBeGreaterThan(0) - }) - }) + parser.push('event: ignored\n\nid: stable\n') + parser.finish('data: final') - describe('原始 fetch 调用', () => { - test('正确传递参数给原始 fetch', async () => { - const mockFetch = vi.fn().mockResolvedValue(createMockResponse()) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const options: RequestInit = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ key: 'value' }) - } - await proxyFn('https://example.com/api', options) - - expect(mockFetch).toHaveBeenCalledWith('https://example.com/api', options) - }) - }) + expect(messages).toEqual([{ eventName: 'message', eventId: 'stable', data: 'final' }]) + }) - describe('SSE (Server-Sent Events) 处理', () => { - // Helper to create a mock SSE response - function createMockSSEResponse(events: string[], options: { delay?: number } = {}) { - const { delay = 0 } = options - const mockHeaders = new Headers({ - 'content-type': 'text/event-stream' - }) - - // Create a mock ReadableStream - let readerIndex = 0 - const encoder = new TextEncoder() - const chunks = events.map((event) => encoder.encode(event)) - - const mockReader = { - read: vi.fn().mockImplementation(async () => { - if (delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) - } - if (readerIndex >= chunks.length) { - return { done: true, value: undefined } - } - const value = chunks[readerIndex] - readerIndex++ - return { done: false, value } - }) - } - - const mockBody = { - getReader: vi.fn().mockReturnValue(mockReader) - } - - return { - status: 200, - headers: mockHeaders, - body: mockBody, - clone: vi.fn().mockReturnValue({ - body: mockBody, - arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)) - }) - } as unknown as Response - } + test('streams SSE messages across chunks before one successful terminal event', async () => { + const chunks = [ + 'id: 7\r', + '\nevent: update\r\ndata: first\r', + '\ndata: second\r\n\r', + '\ndata: tail' + ] + const response = streamResponse(chunks) + const { journal, mainProcess } = createMainProcess() + + await fetchProxyFactory(fetchMock(response), mainProcess)('http://127.0.0.1:43777/events') + const terminal = await waitFor(journal, 'endRequest') + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'eventSourceResponseReceived', + 'eventSourceMessage', + 'eventSourceMessage', + 'endRequest' + ]) + expect( + journal + .filter(({ type }) => type === 'eventSourceMessage') + .map(({ data }) => ({ eventName: data.eventName, eventId: data.eventId, data: data.data })) + ).toEqual([ + { eventName: 'update', eventId: '7', data: 'first\nsecond' }, + { eventName: 'message', eventId: '7', data: 'tail' } + ]) + expect(Buffer.from(terminal.data.responseData).toString()).toBe(chunks.join('')) + }) - test('正确识别 SSE 响应 (text/event-stream)', async () => { - const mockResponse = createMockSSEResponse(['data: hello\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - const result = await proxyFn('https://example.com/sse') - - // SSE 响应应该直接返回,不等待流结束 - expect(result).toBe(mockResponse) - }) - - test('解析简单的 SSE 事件', async () => { - const mockResponse = createMockSSEResponse(['data: hello world\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - // 等待流处理完成 - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(mockSend).toHaveBeenCalledWith({ - type: 'eventSourceMessage', - data: { - requestId: expect.any(String), - eventName: 'message', - eventId: '', - data: 'hello world' - } - }) - }) - - test('解析带有 event 类型的 SSE 事件', async () => { - const mockResponse = createMockSSEResponse(['event: custom\ndata: test data\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(mockSend).toHaveBeenCalledWith({ - type: 'eventSourceMessage', - data: { - requestId: expect.any(String), - eventName: 'custom', - eventId: '', - data: 'test data' - } - }) - }) - - test('解析带有 id 的 SSE 事件', async () => { - const mockResponse = createMockSSEResponse(['id: 123\ndata: with id\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(mockSend).toHaveBeenCalledWith({ - type: 'eventSourceMessage', - data: { - requestId: expect.any(String), - eventName: 'message', - eventId: '123', - data: 'with id' - } - }) - }) - - test('处理多行 data', async () => { - const mockResponse = createMockSSEResponse(['data: line1\ndata: line2\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(mockSend).toHaveBeenCalledWith({ - type: 'eventSourceMessage', - data: { - requestId: expect.any(String), - eventName: 'message', - eventId: '', - data: 'line1\nline2' - } - }) - }) - - test('处理多个连续的 SSE 事件', async () => { - const mockResponse = createMockSSEResponse([ - 'data: first\n\n', - 'data: second\n\n', - 'data: third\n\n' - ]) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const eventSourceCalls = mockSend.mock.calls.filter( - (call) => call[0]?.type === 'eventSourceMessage' - ) - expect(eventSourceCalls.length).toBe(3) - expect(eventSourceCalls[0][0].data.data).toBe('first') - expect(eventSourceCalls[1][0].data.data).toBe('second') - expect(eventSourceCalls[2][0].data.data).toBe('third') - }) - - test('SSE 流结束后发送 endRequest', async () => { - const mockResponse = createMockSSEResponse(['data: test\n\n']) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/sse') - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(mockSendRequest).toHaveBeenCalledWith('endRequest', expect.any(RequestDetail)) - }) - - test('非 SSE 响应不触发 eventSourceMessage', async () => { - const mockResponse = createMockResponse({ - headers: { 'content-type': 'application/json' }, - body: '{"key": "value"}' - }) - const mockFetch = vi.fn().mockResolvedValue(mockResponse) - const mockSend = vi.fn() - const mockSendRequest = vi.fn().mockReturnThis() - const mockMainProcess = { - sendRequest: mockSendRequest, - send: mockSend - } - - const proxyFn = fetchProxyFactory(mockFetch, mockMainProcess as never) - await proxyFn('https://example.com/api') - - await new Promise((resolve) => setTimeout(resolve, 50)) - - const eventSourceCalls = mockSend.mock.calls.filter( - (call) => call[0]?.type === 'eventSourceMessage' - ) - expect(eventSourceCalls.length).toBe(0) - }) - }) + test('clears only the async-context cell owned by the completing fetch', async () => { + let resolveFirst!: (response: Response) => void + const firstFetch = vi.fn( + () => new Promise((resolve) => (resolveFirst = resolve)) + ) as unknown as typeof fetch + const { mainProcess } = createMainProcess() + const firstPromise = fetchProxyFactory(firstFetch, mainProcess)('https://example.test/first') + const firstCell = cellState.current + + cellState.current = { request: 'newer context' } + resolveFirst(new Response('done')) + await firstPromise + + expect(cellState.current).toEqual({ request: 'newer context' }) + expect(vi.mocked(cellModule.setCurrentCell)).not.toHaveBeenCalledWith(null) + expect(firstCell).not.toBeNull() }) }) diff --git a/packages/network-debugger/src/core/fetch.ts b/packages/network-debugger/src/core/fetch.ts index a9326eb..4301a24 100644 --- a/packages/network-debugger/src/core/fetch.ts +++ b/packages/network-debugger/src/core/fetch.ts @@ -1,243 +1,266 @@ import { RequestDetail } from '../common' +import { findFetchMock, mockedFetchResponse, type LegacyMockRule } from '../mock' import { headersToObject } from '../utils/map' -import { MainProcess } from './fork' -import { setCurrentCell } from './hooks/cell' +import type { MainProcess } from './fork' +import { getCurrentCell, setCurrentCell, type Cell } from './hooks/cell' +import { isLegacyCaptureSuppressed } from './capture-scope' -export function proxyFetch(mainProcess: MainProcess) { - if (!globalThis.fetch) { - return - } +export function proxyFetch(mainProcess: MainProcess, mockRules: readonly LegacyMockRule[] = []) { + if (!globalThis.fetch) return const originalFetch = globalThis.fetch - - globalThis['fetch'] = fetchProxyFactory(originalFetch, mainProcess) + const proxy = + mockRules.length > 0 + ? fetchProxyFactory(originalFetch, mainProcess, mockRules) + : fetchProxyFactory(originalFetch, mainProcess) + globalThis.fetch = proxy return () => { - globalThis['fetch'] = originalFetch + if (globalThis.fetch === proxy) globalThis.fetch = originalFetch } } -export function fetchProxyFactory(fetchFn: typeof fetch, mainProcess: MainProcess) { - return function (request: string | URL | Request, options?: RequestInit) { - const requestDetail = new RequestDetail() - requestDetail.requestStartTime = Date.now() - setCurrentCell({ request: requestDetail, pipes: [], isAborted: false }) +interface SseMessage { + eventName: string + eventId: string + data: string +} - if (typeof request === 'string') { - requestDetail.url = request - } else if (request instanceof URL) { - requestDetail.url = request.toString() +/** Incremental WHATWG event-stream parser; fields may span arbitrary chunks. */ +export class SseParser { + private buffer = '' + private eventName = 'message' + private eventId = '' + private data: string[] = [] + private sawData = false + + constructor(private readonly emit: (message: SseMessage) => void) {} + + push(text: string): void { + this.buffer += text + this.drain(false) + } + + finish(text = ''): void { + this.buffer += text + this.drain(true) + if (this.buffer) { + this.line(this.buffer) + this.buffer = '' } + this.dispatch() + } - requestDetail.method = options?.method ?? 'GET' + private drain(final: boolean): void { + while (true) { + const match = /\r\n|\r|\n/.exec(this.buffer) + if (!match) return + // CRLF is one line ending even when the transport splits it across + // chunks. Hold a trailing CR until the next chunk (or EOF) decides it. + if (!final && match[0] === '\r' && match.index === this.buffer.length - 1) return + const line = this.buffer.slice(0, match.index) + this.buffer = this.buffer.slice(match.index + match[0].length) + this.line(line) + } + } - const headers = options?.headers - if (headers instanceof Headers) { - const headersObj = headersToObject(headers) - requestDetail.requestHeaders = headersObj - } else { - requestDetail.requestHeaders = headers ?? {} + private line(line: string): void { + if (line === '') { + this.dispatch() + return } - requestDetail.requestData = options?.body + if (line.startsWith(':')) return - requestDetail.loadCallFrames() + const separator = line.indexOf(':') + const field = separator === -1 ? line : line.slice(0, separator) + let value = separator === -1 ? '' : line.slice(separator + 1) + if (value.startsWith(' ')) value = value.slice(1) - const result = fetchFn(request as string | Request, options) - .then(fetchResponseHandlerFactory(requestDetail, mainProcess)) - .catch(fetchErrorHandlerFactory(requestDetail, mainProcess)) - .finally(() => { - setCurrentCell(null) + if (field === 'event') { + this.eventName = value || 'message' + } else if (field === 'data') { + this.sawData = true + this.data.push(value) + } else if (field === 'id' && !value.includes('\0')) { + this.eventId = value + } + } + + private dispatch(): void { + if (this.sawData) { + this.emit({ + eventName: this.eventName || 'message', + eventId: this.eventId, + data: this.data.join('\n') }) + } + this.eventName = 'message' + this.data = [] + this.sawData = false + } +} - mainProcess - .sendRequest('initRequest', requestDetail) - .sendRequest('registerRequest', requestDetail) +function bodyValue(body: BodyInit | null | undefined): unknown { + if (body === undefined || body === null) return undefined + if (typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array) return body + if (body instanceof URLSearchParams) return body.toString() + return undefined +} - return result +function populateFetchRequest( + detail: RequestDetail, + request: string | URL | Request, + options?: RequestInit +): void { + if (typeof request === 'string') detail.url = request + else if (request instanceof URL) detail.url = request.toString() + else detail.url = request.url + + detail.method = options?.method ?? (request instanceof Request ? request.method : 'GET') + const headers = new Headers(request instanceof Request ? request.headers : undefined) + if (options?.headers) { + new Headers(options.headers).forEach((value, key) => headers.set(key, value)) } + detail.requestHeaders = headersToObject(headers) + detail.requestData = bodyValue(options?.body) + detail.requestStartTime = Date.now() / 1000 + detail.responseHeaders = {} + detail.loadCallFrames() +} + +function failureText(error: unknown): string { + return error instanceof Error ? error.message : String(error || 'Fetch failed') +} + +function responseInto(detail: RequestDetail, response: Response): void { + detail.responseHeaders = headersToObject(response.headers) + detail.responseStatusCode = response.status || 0 + ;(detail as RequestDetail & { responseStatusText?: string }).responseStatusText = + response.statusText } -/** - * Check if the response is a Server-Sent Events (SSE) stream - */ function isEventStream(response: Response): boolean { - const contentType = response.headers.get('content-type') || '' - return contentType.includes('text/event-stream') + return (response.headers.get('content-type') ?? '').includes('text/event-stream') } -/** - * Handle SSE (Server-Sent Events) streaming response - */ -async function handleEventStreamResponse( +async function captureEventStream( response: Response, - requestDetail: RequestDetail, + detail: RequestDetail, mainProcess: MainProcess ): Promise { - const body = response.clone().body - if (!body) { + const stream = response.clone().body + if (!stream) { + detail.responseData = Buffer.alloc(0) + detail.responseInfo = { dataLength: 0, encodedDataLength: 0 } + mainProcess.sendRequest('endRequest', detail) return } - const reader = body.getReader() + const chunks: Buffer[] = [] const decoder = new TextDecoder() - let buffer = '' - const allChunks: Uint8Array[] = [] - - // Send responseReceived first (type: EventSource) so DevTools knows this is an SSE request - mainProcess.send({ - type: 'eventSourceResponseReceived', - data: requestDetail + const parser = new SseParser((message) => { + void mainProcess.send({ + type: 'eventSourceMessage', + data: { requestId: detail.id, ...message } + }) }) + const reader = stream.getReader() try { while (true) { const { done, value } = await reader.read() - - if (done) { - break - } - - if (value) { - allChunks.push(value) - buffer += decoder.decode(value, { stream: true }) - - // Parse SSE events from buffer - const lines = buffer.split('\n') - buffer = lines.pop() || '' // Keep incomplete line in buffer - - let currentEventType = 'message' - let currentEventData = '' - let currentEventId = '' - - for (const line of lines) { - if (line.startsWith('event:')) { - currentEventType = line.slice(6).trim() - } else if (line.startsWith('data:')) { - currentEventData += (currentEventData ? '\n' : '') + line.slice(5).trim() - } else if (line.startsWith('id:')) { - currentEventId = line.slice(3).trim() - } else if (line === '') { - // Empty line means end of event - if (currentEventData) { - mainProcess.send({ - type: 'eventSourceMessage', - data: { - requestId: requestDetail.id, - eventName: currentEventType, - eventId: currentEventId, - data: currentEventData - } - }) - } - // Reset for next event - currentEventType = 'message' - currentEventData = '' - currentEventId = '' - } - } - } - } - - // Handle any remaining data in buffer - if (buffer.trim()) { - const lines = buffer.split('\n') - let currentEventType = 'message' - let currentEventData = '' - let currentEventId = '' - - for (const line of lines) { - if (line.startsWith('event:')) { - currentEventType = line.slice(6).trim() - } else if (line.startsWith('data:')) { - currentEventData += (currentEventData ? '\n' : '') + line.slice(5).trim() - } else if (line.startsWith('id:')) { - currentEventId = line.slice(3).trim() - } - } - - if (currentEventData) { - mainProcess.send({ - type: 'eventSourceMessage', - data: { - requestId: requestDetail.id, - eventName: currentEventType, - eventId: currentEventId, - data: currentEventData - } - }) - } + if (done) break + if (!value) continue + chunks.push(Buffer.from(value)) + parser.push(decoder.decode(value, { stream: true })) } + parser.finish(decoder.decode()) + const body = Buffer.concat(chunks) + detail.responseData = body + detail.responseInfo = { dataLength: body.length, encodedDataLength: body.length } + detail.requestEndTime = Date.now() / 1000 + mainProcess.sendRequest('endRequest', detail) + } catch (error) { + detail.requestEndTime = Date.now() / 1000 + await mainProcess.send({ + type: 'requestFailed', + data: { request: detail, errorText: failureText(error), canceled: true } + }) + } +} - // Combine all chunks for final response data - const totalLength = allChunks.reduce((acc, chunk) => acc + chunk.length, 0) - const combinedArray = new Uint8Array(totalLength) - let offset = 0 - for (const chunk of allChunks) { - combinedArray.set(chunk, offset) - offset += chunk.length - } +function captureResponse( + response: Response, + detail: RequestDetail, + mainProcess: MainProcess +): void { + responseInto(detail, response) + void mainProcess.send({ + type: isEventStream(response) ? 'eventSourceResponseReceived' : 'responseReceived', + data: detail + }) - requestDetail.responseData = Buffer.from(combinedArray) - requestDetail.responseInfo.dataLength = totalLength - requestDetail.responseInfo.encodedDataLength = totalLength - } catch (error) { - // Stream was aborted or errored, still try to save what we have - if (allChunks.length > 0) { - const totalLength = allChunks.reduce((acc, chunk) => acc + chunk.length, 0) - const combinedArray = new Uint8Array(totalLength) - let offset = 0 - for (const chunk of allChunks) { - combinedArray.set(chunk, offset) - offset += chunk.length - } - requestDetail.responseData = Buffer.from(combinedArray) - requestDetail.responseInfo.dataLength = totalLength - requestDetail.responseInfo.encodedDataLength = totalLength - } - } finally { - requestDetail.requestEndTime = Date.now() - mainProcess.sendRequest('updateRequest', requestDetail).sendRequest('endRequest', requestDetail) + if (isEventStream(response)) { + void captureEventStream(response, detail, mainProcess) + return } + + void response + .clone() + .arrayBuffer() + .then((arrayBuffer) => { + const body = Buffer.from(arrayBuffer) + detail.responseData = body + detail.responseInfo = { dataLength: body.length, encodedDataLength: body.length } + detail.requestEndTime = Date.now() / 1000 + mainProcess.sendRequest('endRequest', detail) + }) + .catch(async (error) => { + detail.requestEndTime = Date.now() / 1000 + await mainProcess.send({ + type: 'requestFailed', + data: { request: detail, errorText: failureText(error), canceled: true } + }) + }) } -function fetchResponseHandlerFactory(requestDetail: RequestDetail, mainProcess: MainProcess) { - return (response: Response) => { - requestDetail.requestEndTime = new Date().getTime() - requestDetail.responseHeaders = headersToObject(response.headers) - requestDetail.responseStatusCode = response.status || 0 - - // Check if this is an SSE stream - if (isEventStream(response)) { - // Handle SSE asynchronously without blocking the response - handleEventStreamResponse(response, requestDetail, mainProcess) - return response +export function fetchProxyFactory( + fetchFn: typeof fetch, + mainProcess: MainProcess, + mockRules: readonly LegacyMockRule[] = [] +): typeof fetch { + return function fetchProxy(request: string | URL | Request, options?: RequestInit) { + if (isLegacyCaptureSuppressed()) { + return fetchFn(request as string | Request, options) } + const detail = new RequestDetail() + populateFetchRequest(detail, request, options) + const cell: Cell = { request: detail, pipes: [], isAborted: false } + setCurrentCell(cell) - // Handle regular response - response - .clone() - .arrayBuffer() - .then((buffer) => { - const responseData = Buffer.from(buffer) - requestDetail.responseData = responseData - requestDetail.responseInfo.dataLength = responseData.length - // TODO: use content-encoding to determine the actual length - requestDetail.responseInfo.encodedDataLength = responseData.length - }) - .finally(() => { - mainProcess - .sendRequest('updateRequest', requestDetail) - .sendRequest('endRequest', requestDetail) - }) + mainProcess.sendRequest('initRequest', detail).sendRequest('registerRequest', detail) - return response - } -} + const mockRule = findFetchMock(mockRules, request, options) + const responsePromise = mockRule + ? mockedFetchResponse(mockRule, options?.signal) + : fetchFn(request as string | Request, options) -function fetchErrorHandlerFactory(requestDetail: RequestDetail, mainProcess: MainProcess) { - return (err: unknown) => { - requestDetail.requestEndTime = Date.now() - requestDetail.responseStatusCode = 0 - mainProcess.sendRequest('updateRequest', requestDetail).sendRequest('endRequest', requestDetail) - throw err - } + return responsePromise + .then( + (response) => { + captureResponse(response, detail, mainProcess) + return response + }, + async (error) => { + detail.requestEndTime = Date.now() / 1000 + await mainProcess.send({ + type: 'requestFailed', + data: { request: detail, errorText: failureText(error) } + }) + throw error + } + ) + .finally(() => { + if (getCurrentCell() === cell) setCurrentCell(null) + }) + } as typeof fetch } diff --git a/packages/network-debugger/src/core/fork.test.ts b/packages/network-debugger/src/core/fork.test.ts index 5b4e060..831bb03 100644 --- a/packages/network-debugger/src/core/fork.test.ts +++ b/packages/network-debugger/src/core/fork.test.ts @@ -1,597 +1,259 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import { EventEmitter } from 'events' -import { RequestDetail, READY_MESSAGE } from '../common' -import type { IncomingMessage } from 'http' - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { - mockWsSend, - mockWsTerminate, - mockWsRemoveAllListeners, - mockFork, - mockCpKill, - mockCpRemoveAllListeners, - mockExistsSync, - mockReadFileSync, - mockWriteFileSync, - mockSleep, - mockCheckMainProcessAlive, - mockUnlinkSafe, - mockWarn, - mockGetCurrentCell, - mockGenerateUUID, - wsInstances, - cpInstances -} = vi.hoisted(() => { - let uuidCounter = 0 - return { - mockWsSend: vi.fn(), - mockWsTerminate: vi.fn(), - mockWsRemoveAllListeners: vi.fn(), - mockFork: vi.fn(), - mockCpKill: vi.fn(), - mockCpRemoveAllListeners: vi.fn(), - mockExistsSync: vi.fn().mockReturnValue(false), - mockReadFileSync: vi.fn().mockReturnValue('12345'), - mockWriteFileSync: vi.fn(), - mockSleep: vi.fn().mockResolvedValue(undefined), - mockCheckMainProcessAlive: vi.fn().mockResolvedValue(false), - mockUnlinkSafe: vi.fn(), - mockWarn: vi.fn(), - mockGetCurrentCell: vi.fn().mockReturnValue(null), - mockGenerateUUID: vi.fn().mockImplementation(() => `mock-uuid-${++uuidCounter}`), - wsInstances: [] as EventEmitter[], - cpInstances: [] as EventEmitter[] +import { EventEmitter } from 'node:events' +import { describe, expect, test, vi } from 'vitest' +import type { DevtoolsTarget, Diagnostic } from '../adapters/types' +import { RequestDetail } from '../common' +import type { LegacyBridgeError } from '../legacy-bridge/client' +import type { LegacyCaptureEvent } from '../legacy-bridge/contracts' +import { MainProcess } from './fork' +import { setCurrentCell } from './hooks/cell' + +const target: DevtoolsTarget = { + id: 'legacy', + title: 'Legacy', + type: 'node', + url: '', + webSocketDebuggerUrl: 'ws://127.0.0.1:43120/devtools/page/legacy', + discoveryUrl: 'http://127.0.0.1:43120/json/list' +} + +function bridgeHarness() { + const events: LegacyCaptureEvent[] = [] + const listeners = new Set<(diagnostic: Diagnostic) => void>() + const failureListeners = new Set<(error: LegacyBridgeError) => void>() + const bridge = { + ready: Promise.resolve(target), + send: vi.fn(async (event: LegacyCaptureEvent) => { + events.push(event) + }), + onDiagnostic: vi.fn((listener: (diagnostic: Diagnostic) => void) => { + listeners.add(listener) + return () => listeners.delete(listener) + }), + onFailure: vi.fn((listener: (error: LegacyBridgeError) => void) => { + failureListeners.add(listener) + return () => failureListeners.delete(listener) + }), + dispose: vi.fn(async () => undefined) } -}) - -// Mock ws 模块 - 使用正确的继承方式 -vi.mock('ws', () => { - const { EventEmitter } = require('events') + const main = new MainProcess({ key: 'compat-key', port: 5270, serverPort: 0 }, { bridge }) + return { main, bridge, events, listeners, failureListeners } +} + +function request(id = 'request-1'): RequestDetail { + const detail = new RequestDetail() + detail.id = id + detail.url = 'http://example.test/' + detail.method = 'GET' + detail.requestHeaders = {} + return detail +} + +class FakeResponse extends EventEmitter { + statusCode = 200 + statusMessage = 'OK' + headers = { 'content-type': 'text/plain', 'content-encoding': 'gzip' } + complete = false +} + +describe('MainProcess IPC compatibility facade', () => { + test('exposes ready/diagnostics and keeps the chainable sendRequest API', async () => { + const { main, bridge, events, listeners, failureListeners } = bridgeHarness() + const detail = request() + const onFailure = vi.fn() + main.onFailure(onFailure) + expect(failureListeners.has(onFailure)).toBe(true) + const diagnosticListener = vi.fn() + + expect(main.onDiagnostic(diagnosticListener)).toEqual(expect.any(Function)) + expect(listeners.has(diagnosticListener)).toBe(true) + expect(main.sendRequest('initRequest', detail)).toBe(main) + expect(main.sendRequest('registerRequest', detail)).toBe(main) + await expect(main.ready).resolves.toEqual(target) + expect(events.map((event) => event.type)).toEqual(['initRequest', 'registerRequest']) + + await main.dispose() + expect(bridge.dispose).toHaveBeenCalledOnce() + }) - function MockWebSocket(this: EventEmitter) { - EventEmitter.call(this) - this.send = mockWsSend - this.terminate = mockWsTerminate - const originalRemoveAllListeners = this.removeAllListeners.bind(this) - this.removeAllListeners = function () { - mockWsRemoveAllListeners() - return originalRemoveAllListeners() + test('binds pipes and abort state by request id across concurrent async lifecycles', async () => { + const { main, events } = bridgeHarness() + const first = request('concurrent-a') + const second = request('concurrent-b') + const firstCell = { + request: first, + isAborted: false, + pipes: [ + { + type: 'updateRequest' as const, + pipe: (detail: RequestDetail) => Object.assign(new RequestDetail(detail), { method: 'A' }) + } + ] } - wsInstances.push(this) - } - - // 正确继承 EventEmitter - MockWebSocket.prototype = Object.create(EventEmitter.prototype) - MockWebSocket.prototype.constructor = MockWebSocket - - return { - default: MockWebSocket - } -}) - -// Mock child_process 模块 -vi.mock('child_process', () => { - const { EventEmitter } = require('events') - - return { - fork: function () { - mockFork() - const cp = new EventEmitter() - cp.send = vi.fn().mockReturnValue(true) - cp.kill = mockCpKill - const originalRemoveAllListeners = cp.removeAllListeners.bind(cp) - cp.removeAllListeners = function () { - mockCpRemoveAllListeners() - return originalRemoveAllListeners() - } - cpInstances.push(cp) - return cp + const secondCell = { + request: second, + isAborted: false, + pipes: [ + { + type: 'updateRequest' as const, + pipe: (detail: RequestDetail) => Object.assign(new RequestDetail(detail), { method: 'B' }) + } + ] } - } -}) - -// Mock fs 模块 -vi.mock('fs', () => ({ - default: { - existsSync: mockExistsSync, - readFileSync: mockReadFileSync, - writeFileSync: mockWriteFileSync - } -})) - -// Mock utils/process 模块 -vi.mock('../utils/process', () => ({ - sleep: mockSleep, - checkMainProcessAlive: mockCheckMainProcessAlive -})) -// Mock utils/file 模块 -vi.mock('../utils/file', () => ({ - unlinkSafe: mockUnlinkSafe -})) - -// Mock utils 模块 - 添加 generateUUID -vi.mock('../utils', () => ({ - warn: mockWarn, - generateUUID: mockGenerateUUID -})) - -// Mock hooks/cell 模块 -vi.mock('./hooks/cell', () => ({ - getCurrentCell: mockGetCurrentCell -})) - -describe('core/fork.ts', () => { - beforeEach(() => { - vi.useFakeTimers() - vi.clearAllMocks() - wsInstances.length = 0 - cpInstances.length = 0 - mockExistsSync.mockReturnValue(false) - mockCheckMainProcessAlive.mockResolvedValue(false) - mockGetCurrentCell.mockReturnValue(null) - }) - - afterEach(() => { - vi.useRealTimers() - vi.restoreAllMocks() + setCurrentCell(firstCell) + main.sendRequest('initRequest', first) + setCurrentCell(secondCell) + main.sendRequest('initRequest', second) + // The global cell now belongs to B; A must still use its own pipe/state. + main.sendRequest('updateRequest', first) + firstCell.isAborted = true + main.sendRequest('updateRequest', first) + main.sendRequest('updateRequest', second) + setCurrentCell(null) + + const updates = events.filter( + (event): event is Extract => + event.type === 'updateRequest' + ) + expect(updates.map((event) => [event.data.id, event.data.method])).toEqual([ + ['concurrent-a', 'A'], + ['concurrent-b', 'B'] + ]) + await main.dispose() }) - describe('MainProcess 类', () => { - describe('构造函数', () => { - test('当 lock 文件不存在时,创建新的 WebSocket 连接并写入 lock 文件', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 验证 lock 文件被写入 - expect(mockWriteFileSync).toHaveBeenCalledWith( - expect.stringContaining('test-key'), - expect.stringContaining(String(process.pid)) - ) - - // 验证 WebSocket 被创建 - expect(wsInstances.length).toBe(1) - }) - - test('WebSocket 连接成功后删除 lock 文件', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 验证 lock 文件被删除 - expect(mockUnlinkSafe).toHaveBeenCalled() - }) - - test('当 lock 文件存在且进程存活时,跳过创建并输出警告', async () => { - mockExistsSync.mockReturnValue(true) - mockReadFileSync.mockReturnValue('12345') - mockCheckMainProcessAlive.mockResolvedValue(true) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 运行所有待处理的定时器和微任务 - await vi.runAllTimersAsync() - - // 验证警告被输出 - expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('already running')) - }) - - test('当 lock 文件存在但进程不存活时,删除 lock 文件并继续', async () => { - mockExistsSync.mockReturnValue(true) - mockReadFileSync.mockReturnValue('12345') - mockCheckMainProcessAlive.mockResolvedValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 运行所有待处理的定时器和微任务 - await vi.runAllTimersAsync() - - // 验证 lock 文件被删除 - expect(mockUnlinkSafe).toHaveBeenCalled() - }) - - test('WebSocket 连接错误时,启动子进程', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接错误 - wsInstances[0].emit('error', new Error('Connection refused')) - - // 验证 fork 被调用 - expect(mockFork).toHaveBeenCalled() - }) - - test('子进程发送 READY_MESSAGE 后创建新的 WebSocket 连接', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - const initialWsCount = wsInstances.length - - // 模拟 WebSocket 连接错误 - wsInstances[0].emit('error', new Error('Connection refused')) - - // 模拟子进程发送 ready 消息 - cpInstances[0].emit('message', READY_MESSAGE) - - // 验证新的 WebSocket 连接被创建 - expect(wsInstances.length).toBeGreaterThan(initialWsCount) - }) - - test('子进程发送非 READY_MESSAGE 时不创建新连接', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接错误 - wsInstances[0].emit('error', new Error('Connection refused')) - - const wsCountAfterError = wsInstances.length - - // 模拟子进程发送其他消息 - cpInstances[0].emit('message', 'other-message') - - // 验证没有创建新的 WebSocket 连接 - expect(wsInstances.length).toBe(wsCountAfterError) - }) - - test('WebSocket 错误事件被正确记录', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 先连接成功 - wsInstances[0].emit('open') - - // 等待 Promise 解析 - await vi.advanceTimersByTimeAsync(0) - - // 然后发生错误 - const error = new Error('WebSocket error') - wsInstances[0].emit('error', error) - - // 验证错误被记录 - expect(consoleSpy).toHaveBeenCalledWith('MainProcess Socket Error: ', error) - - consoleSpy.mockRestore() - }) - }) - - describe('send 方法', () => { - test('发送数据到 WebSocket', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 发送数据 - const testData = { type: 'test', data: { foo: 'bar' } } - await mainProcess.send(testData) - - // 验证数据被发送 - expect(mockWsSend).toHaveBeenCalledWith(JSON.stringify(testData)) - }) - - test('当 cell 被中止时,不发送数据', async () => { - mockExistsSync.mockReturnValue(false) - mockGetCurrentCell.mockReturnValue({ - isAborted: true, - request: new RequestDetail(), - pipes: [] - }) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 清除之前的调用 - mockWsSend.mockClear() - - // 发送数据 - await mainProcess.send({ type: 'test' }) - - // 验证数据没有被发送 - expect(mockWsSend).not.toHaveBeenCalled() - }) - }) - - describe('sendRequest 方法', () => { - test('发送请求数据', async () => { - mockExistsSync.mockReturnValue(false) - mockGetCurrentCell.mockReturnValue(null) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 等待 Promise 解析 - await vi.advanceTimersByTimeAsync(0) - - // 清除之前的调用 - mockWsSend.mockClear() - - // 发送请求 - const requestDetail = new RequestDetail() - requestDetail.url = 'http://example.com' - requestDetail.method = 'GET' - - mainProcess.sendRequest('initRequest', requestDetail) - - // 等待异步发送完成 - await vi.advanceTimersByTimeAsync(0) - - // 验证数据被发送 - expect(mockWsSend).toHaveBeenCalledWith(expect.stringContaining('initRequest')) - }) - - test('返回 this 以支持链式调用', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - const requestDetail = new RequestDetail() - const result = mainProcess.sendRequest('initRequest', requestDetail) - - expect(result).toBe(mainProcess) - }) - - test('当存在 cell 时,应用 pipes', async () => { - mockExistsSync.mockReturnValue(false) - - const mockPipe = vi.fn((req: RequestDetail) => { - req.method = 'POST' - return req - }) - - mockGetCurrentCell.mockReturnValue({ - isAborted: false, - request: new RequestDetail(), - pipes: [{ type: 'initRequest', pipe: mockPipe }] - }) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - const requestDetail = new RequestDetail() - requestDetail.method = 'GET' - - mainProcess.sendRequest('initRequest', requestDetail) - - // 验证 pipe 被调用 - expect(mockPipe).toHaveBeenCalled() - }) - - test('只应用匹配类型的 pipes', async () => { - mockExistsSync.mockReturnValue(false) - - const initPipe = vi.fn((req: RequestDetail) => req) - const updatePipe = vi.fn((req: RequestDetail) => req) - - mockGetCurrentCell.mockReturnValue({ - isAborted: false, - request: new RequestDetail(), - pipes: [ - { type: 'initRequest', pipe: initPipe }, - { type: 'updateRequest', pipe: updatePipe } - ] - }) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - const requestDetail = new RequestDetail() - mainProcess.sendRequest('initRequest', requestDetail) - - // 验证只有 initPipe 被调用 - expect(initPipe).toHaveBeenCalled() - expect(updatePipe).not.toHaveBeenCalled() - }) + test('strips IncomingMessage/socket state from the WebSocket handshake event', async () => { + const { main, events } = bridgeHarness() + const response = Object.assign(new EventEmitter(), { + httpVersion: '1.1', + statusCode: 101, + statusMessage: 'Switching Protocols', + rawHeaders: ['Upgrade', 'websocket'], + headers: { upgrade: 'websocket' }, + socket: { live: true } }) - describe('responseRequest 方法', () => { - test('处理响应数据并发送', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 等待 Promise 解析 - await vi.advanceTimersByTimeAsync(0) - - // 清除之前的调用 - mockWsSend.mockClear() - - // 创建 mock 响应 - const mockResponse = new EventEmitter() as EventEmitter & { - statusCode: number - headers: Record - } - mockResponse.statusCode = 200 - mockResponse.headers = { 'content-type': 'application/json' } - - // 调用 responseRequest - mainProcess.responseRequest('test-id', mockResponse as IncomingMessage) - - // 模拟响应数据 - mockResponse.emit('data', Buffer.from('{"foo":"bar"}')) - mockResponse.emit('end') - - // 等待异步操作完成 - await vi.advanceTimersByTimeAsync(0) - - // 验证数据被发送 - expect(mockWsSend).toHaveBeenCalledWith(expect.stringContaining('responseData'), { - binary: true - }) - - // 验证发送的数据包含正确的 id 和状态码 - const sentData = JSON.parse(mockWsSend.mock.calls[0][0]) - expect(sentData.type).toBe('responseData') - expect(sentData.data.id).toBe('test-id') - expect(sentData.data.statusCode).toBe(200) - }) - - test('正确合并多个数据块', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 等待 Promise 解析 - await vi.advanceTimersByTimeAsync(0) - - // 清除之前的调用 - mockWsSend.mockClear() - - const mockResponse = new EventEmitter() as EventEmitter & { - statusCode: number - headers: Record + await main.send({ + type: 'Network.webSocketCreated', + data: { requestId: 'ws-1', url: 'ws://example.test/', response } + } as any) + + expect(events).toEqual([ + { + type: 'Network.webSocketCreated', + data: { + requestId: 'ws-1', + url: 'ws://example.test/', + response: { + httpVersion: '1.1', + statusCode: 101, + statusMessage: 'Switching Protocols', + rawHeaders: ['Upgrade', 'websocket'], + headers: { upgrade: 'websocket' } + } } - mockResponse.statusCode = 200 - mockResponse.headers = {} - - mainProcess.responseRequest('test-id', mockResponse as IncomingMessage) - - // 模拟多个数据块 - mockResponse.emit('data', Buffer.from('Hello')) - mockResponse.emit('data', Buffer.from(' ')) - mockResponse.emit('data', Buffer.from('World')) - mockResponse.emit('end') - - await vi.advanceTimersByTimeAsync(0) - - // 验证数据被正确合并 - const sentData = JSON.parse(mockWsSend.mock.calls[0][0]) - const rawData = Buffer.from(sentData.data.rawData.data) - expect(rawData.toString()).toBe('Hello World') - }) - }) - - describe('dispose 方法', () => { - test('清理 WebSocket 连接', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 调用 dispose - await mainProcess.dispose() - - // 验证 WebSocket 被清理 - expect(mockWsRemoveAllListeners).toHaveBeenCalled() - expect(mockWsTerminate).toHaveBeenCalled() - }) - - test('清理子进程', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - const mainProcess = new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接错误,触发子进程创建 - wsInstances[0].emit('error', new Error('Connection refused')) - - // 模拟子进程发送 ready 消息 - cpInstances[0].emit('message', READY_MESSAGE) - - // 模拟新的 WebSocket 连接成功 - wsInstances[1].emit('open') - - // 调用 dispose - await mainProcess.dispose() - - // 验证子进程被清理 - expect(mockCpRemoveAllListeners).toHaveBeenCalled() - expect(mockCpKill).toHaveBeenCalled() - }) - }) - - describe('healthCheck 私有方法', () => { - test('WebSocket 连接成功后发送健康检查消息', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 等待 Promise 解析 - await vi.advanceTimersByTimeAsync(0) - - // 验证健康检查消息被发送 - expect(mockWsSend).toHaveBeenCalledWith(expect.stringContaining('healthcheck')) - }) - - test('定时发送健康检查消息', async () => { - mockExistsSync.mockReturnValue(false) - - const { MainProcess } = await import('./fork') - new MainProcess({ port: 5270, key: 'test-key' }) - - // 模拟 WebSocket 连接成功 - wsInstances[0].emit('open') - - // 清除初始的健康检查调用 - mockWsSend.mockClear() - - // 推进时间 2 秒 - await vi.advanceTimersByTimeAsync(2000) + } + ]) + expect((events[0] as any).data.response.socket).toBeUndefined() + await main.dispose() + }) - // 验证健康检查消息被再次发送 - expect(mockWsSend).toHaveBeenCalledWith(expect.stringContaining('healthcheck')) - }) + test('normalizes the historical method/params WebSocket close shape', async () => { + const { main, events } = bridgeHarness() + await main.send({ + method: 'Network.webSocketClosed', + params: { requestId: 'ws-2', timestamp: 123 } }) + expect(events).toEqual([{ type: 'Network.webSocketClosed', data: { requestId: 'ws-2' } }]) + await main.dispose() }) - describe('RequestType 类型', () => { - test('导出正确的请求类型', async () => { - const { MainProcess } = await import('./fork') + test('emits responseReceived immediately and responseData with a real Buffer only on end', async () => { + const { main, events } = bridgeHarness() + const detail = request('success') + main.sendRequest('registerRequest', detail) + const response = new FakeResponse() + + main.responseRequest('success', response as any) + expect(events.map((event) => event.type)).toEqual(['registerRequest', 'responseReceived']) + expect(events[1].data as RequestDetail & { responseStatusText?: string }).toMatchObject({ + responseStatusCode: 200, + responseStatusText: 'OK' + }) - // 验证 MainProcess 类存在 - expect(MainProcess).toBeDefined() - expect(typeof MainProcess).toBe('function') + response.emit('data', Buffer.from('hello ')) + response.emit('data', new Uint8Array(Buffer.from('world'))) + response.complete = true + response.emit('end') + response.emit('close') + + expect(events.map((event) => event.type)).toEqual([ + 'registerRequest', + 'responseReceived', + 'responseData' + ]) + const result = events[2] as Extract + expect(Buffer.isBuffer(result.data.rawData)).toBe(true) + expect(result.data.rawData.toString()).toBe('hello world') + expect(detail.requestEndTime).toBeGreaterThan(1_000_000_000) + expect(detail.requestEndTime).toBeLessThan(10_000_000_000) + expect(result.data).toMatchObject({ + id: 'success', + statusCode: 200, + statusMessage: 'OK', + contentEncoding: 'gzip' }) + await main.dispose() }) - describe('__dirname 导出', () => { - test('导出 __dirname', async () => { - const forkModule = await import('./fork') - - expect(forkModule.__dirname).toBeDefined() - expect(typeof forkModule.__dirname).toBe('string') - }) + test.each([ + ['aborted', undefined, true], + ['error', new Error('socket reset'), false], + ['close', undefined, false] + ] as const)( + '%s emits requestFailed exactly once and never responseData', + async (eventName, error, canceled) => { + const { main, events } = bridgeHarness() + const detail = request(eventName) + main.sendRequest('registerRequest', detail) + const response = new FakeResponse() + // Real IncomingMessage consumers may keep their own error listener after + // MainProcess removes only the listener it owns. + response.on('error', () => undefined) + main.responseRequest(detail, response as any) + + if (error) response.emit(eventName, error) + else response.emit(eventName) + response.emit('error', new Error('duplicate')) + response.emit('close') + + expect(events.map((event) => event.type)).toEqual([ + 'registerRequest', + 'responseReceived', + 'requestFailed' + ]) + const failed = events[2] as Extract + expect(failed.data.request.id).toBe(eventName) + expect(failed.data.request.requestEndTime).toBeLessThan(10_000_000_000) + expect(Boolean(failed.data.canceled)).toBe(canceled) + expect(events.some((event) => event.type === 'responseData')).toBe(false) + await main.dispose() + } + ) + + test('dispose removes active response listeners and ignores later stream events', async () => { + const { main, bridge, events } = bridgeHarness() + const detail = request('dispose') + main.sendRequest('registerRequest', detail) + const response = new FakeResponse() + main.responseRequest(detail, response as any) + + await main.dispose() + expect(response.listenerCount('data')).toBe(0) + expect(response.listenerCount('end')).toBe(0) + response.emit('data', Buffer.from('ignored')) + response.emit('end') + expect(events.map((event) => event.type)).toEqual(['registerRequest', 'responseReceived']) + expect(bridge.dispose).toHaveBeenCalledOnce() }) }) diff --git a/packages/network-debugger/src/core/fork.ts b/packages/network-debugger/src/core/fork.ts index 13d6c17..c8ead6d 100644 --- a/packages/network-debugger/src/core/fork.ts +++ b/packages/network-debugger/src/core/fork.ts @@ -1,190 +1,296 @@ -import { READY_MESSAGE, RequestDetail } from '../common' -import { type IncomingMessage } from 'http' -import WebSocket from 'ws' -import { ChildProcess, fork } from 'child_process' -import { __dirname } from '../common' -import { resolve as resolvePath } from 'path' -import { RegisterOptions } from '../common' -import fs from 'fs' -import { sleep, checkMainProcessAlive } from '../utils/process' -import { unlinkSafe } from '../utils/file' -import { warn } from '../utils' -import { getCurrentCell } from './hooks/cell' - -class ExpectError extends Error { - constructor(message: string) { - super(message) +import type { IncomingHttpHeaders, IncomingMessage } from 'node:http' +import type { DevtoolsTarget, Diagnostic } from '../adapters/types' +import { RequestDetail, type RegisterOptions } from '../common' +import { + LegacyBridgeClient, + type LegacyBridgeClientDependencies, + type DiagnosticListener, + type FailureListener, + type LegacyBridgeError +} from '../legacy-bridge/client' +import type { + LegacyCaptureEvent, + LegacyCaptureSink, + LegacyRequestEventType, + LegacyResponseData, + LegacyWebSocketHandshake +} from '../legacy-bridge/contracts' +import { getCurrentCell, type Cell } from './hooks/cell' + +export type RequestType = LegacyRequestEventType + +type CapturedIncomingMessage = NodeJS.ReadableStream & { + statusCode?: number + statusMessage?: string + headers: IncomingHttpHeaders + httpVersion?: string + rawHeaders?: string[] + complete?: boolean +} + +interface LegacyBridgeTransport { + readonly ready: Promise + send(event: LegacyCaptureEvent): Promise + onDiagnostic(listener: DiagnosticListener): () => void + onFailure(listener: FailureListener): () => void + dispose(): Promise +} + +export interface MainProcessDependencies extends LegacyBridgeClientDependencies { + bridge?: LegacyBridgeTransport +} + +function headerValue(headers: IncomingHttpHeaders, name: string): string | undefined { + const value = headers[name] + if (Array.isArray(value)) return value[0] + return value === undefined ? undefined : String(value) +} + +function websocketHandshake(response: unknown): LegacyWebSocketHandshake { + const value = (response ?? {}) as Partial & Partial + return { + httpVersion: typeof value.httpVersion === 'string' ? value.httpVersion : '', + statusCode: typeof value.statusCode === 'number' ? value.statusCode : 0, + statusMessage: typeof value.statusMessage === 'string' ? value.statusMessage : '', + rawHeaders: Array.isArray(value.rawHeaders) ? [...value.rawHeaders] : [], + headers: value.headers && typeof value.headers === 'object' ? { ...value.headers } : {} } } /** - * @flow initRequest -> registerRequest -> updateRequest -> endRequest + * Convert the handful of historical capture call shapes at the compatibility + * boundary, and guarantee that sockets/IncomingMessage objects never cross IPC. */ -export type RequestType = 'initRequest' | 'registerRequest' | 'updateRequest' | 'endRequest' - -export class MainProcess { - private ws: Promise - private options: RegisterOptions - private cp?: ChildProcess - - constructor(props: RegisterOptions & { key: string }) { - this.options = props - this.ws = new Promise(async (resolve, reject) => { - const lockFilePath = resolvePath(__dirname, `./${props.key}`) - if (fs.existsSync(lockFilePath)) { - // 读取 lock 文件中的进程号 - const pid = fs.readFileSync(lockFilePath, 'utf-8') - await sleep(1) - - // 检测该进程是否存活且 port 是否被占用 - const isProcessAlice = await checkMainProcessAlive(pid, props.port!) - if (isProcessAlice) { - warn(`The main process with same options is already running, skip it.`) - return - } - // 如果进程不存在: - // 1. 热更新导致 process 重启 - // 2. 上一个进程未成功删除 lock - // 都应该继续往下 - unlinkSafe(lockFilePath) - } - fs.writeFileSync(lockFilePath, `${process.pid}`) - const socket = new WebSocket(`ws://127.0.0.1:${props.port}`) - socket.on('open', () => { - unlinkSafe(lockFilePath) - resolve(socket) - }) - socket.on('error', () => { - this.openProcess(() => { - unlinkSafe(lockFilePath) - const socket = new WebSocket(`ws://127.0.0.1:${props.port}`) - socket.on('open', () => { - resolve(socket) - }) - socket.on('error', reject) - }) - }) - }) - this.ws - .then((ws) => { - this.healthCheck() - ws.on('error', (e) => { - console.error('MainProcess Socket Error: ', e) - }) - }) - .catch((e) => { - if (e instanceof ExpectError) { - return - } - throw e - }) +function normalizeCaptureEvent(input: unknown): LegacyCaptureEvent | undefined { + if (!input || typeof input !== 'object') return undefined + const value = input as Record + + // v1 accidentally used CDP's method/params shape for this one event. + if (value.method === 'Network.webSocketClosed') { + const params = (value.params ?? {}) as { requestId?: unknown } + if (typeof params.requestId !== 'string') return undefined + return { + type: 'Network.webSocketClosed', + data: { requestId: params.requestId } + } } - private openProcess(callback?: (cp: ChildProcess) => void) { - const forkProcess = () => { - // fork a new process with options - const cp = fork(resolvePath(__dirname, './fork'), { - env: { - ...process.env, - NETWORK_OPTIONS: JSON.stringify(this.options) - } - }) - const handleMsg = (e: any) => { - if (e === READY_MESSAGE) { - callback && callback(cp) - cp.off('message', handleMsg) - } + if (typeof value.type !== 'string' || !('data' in value)) return undefined + if (value.type === 'Network.webSocketCreated') { + const data = value.data as Record + if (!data || typeof data.requestId !== 'string' || typeof data.url !== 'string') { + return undefined + } + return { + type: 'Network.webSocketCreated', + data: { + requestId: data.requestId, + url: data.url, + ...(data.initiator ? { initiator: data.initiator as RequestDetail['initiator'] } : {}), + response: websocketHandshake(data.response) } - - cp.on('message', handleMsg) - this.cp = cp } + } + + return input as LegacyCaptureEvent +} + +function requestForId(id: string, known?: RequestDetail): RequestDetail { + if (known) return known + const request = new RequestDetail() + request.id = id + return request +} + +/** + * Compatibility facade used by the existing HTTP/fetch capture patches. + * Application-to-child transport is process IPC with advanced serialization. + */ +export class MainProcess implements LegacyCaptureSink { + readonly ready: Promise + + private readonly bridge: LegacyBridgeTransport + private readonly requests = new Map() + private readonly requestCells = new Map() + private readonly responseCleanups = new Set<() => void>() + private disposed = false - forkProcess() + constructor( + props: RegisterOptions & { key: string }, + dependencies: MainProcessDependencies = {} + ) { + this.bridge = + dependencies.bridge ?? + new LegacyBridgeClient( + { + host: '127.0.0.1', + targetPort: props.serverPort ?? 0, + title: 'Node Network Devtools (Legacy)' + }, + dependencies + ) + this.ready = this.bridge.ready } - public async send(data: any) { - const currentCell = getCurrentCell() - if (currentCell?.isAborted) { - return + onDiagnostic(listener: (diagnostic: Diagnostic) => void): () => void { + return this.bridge.onDiagnostic(listener) + } + + onFailure(listener: (error: LegacyBridgeError) => void): () => void { + return this.bridge.onFailure(listener) + } + + public send(event: LegacyCaptureEvent): Promise + public send(event: unknown): Promise + public send(event: unknown): Promise { + if (this.disposed) return Promise.resolve() + const normalized = normalizeCaptureEvent(event) + if (!normalized) return Promise.resolve() + const sendPromise = this.bridge.send(normalized) + const terminalRequestId = + normalized.type === 'requestFailed' + ? normalized.data.request.id + : normalized.type === 'Network.webSocketClosed' + ? normalized.data.requestId + : undefined + if (terminalRequestId) { + this.requests.delete(terminalRequestId) + this.requestCells.delete(terminalRequestId) } - const ws = await this.ws.catch((err) => { - if (err instanceof ExpectError) { - return null - } - throw err - }) - if (!ws) return - ws.send(JSON.stringify(data)) + return sendPromise } - public sendRequest(type: RequestType, request: RequestDetail) { - const currentCell = getCurrentCell() - let req = request + public sendRequest(type: RequestType, request: RequestDetail): this { + const requestId = request.id + let currentCell = this.requestCells.get(requestId) + if ((type === 'initRequest' || type === 'registerRequest') && !currentCell) { + const candidate = getCurrentCell() + if (candidate?.request.id === requestId) { + currentCell = candidate + this.requestCells.set(requestId, candidate) + } + } + + if (currentCell?.isAborted) { + if (type === 'endRequest') this.requestCells.delete(requestId) + return this + } + + let transformed = request if (currentCell) { - currentCell.request = req - const pipes = currentCell.pipes.filter((p) => p.type === type).map((p) => p.pipe) - pipes.forEach((pipe) => { - req = pipe(req) - }) - currentCell.request = req + currentCell.request = transformed + const pipes = currentCell.pipes.filter((pipe) => pipe.type === type) + for (const { pipe } of pipes) transformed = pipe(transformed) + currentCell.request = transformed } - this.send({ - type, - data: request - }) + if (transformed.id !== requestId && currentCell) { + this.requestCells.delete(requestId) + this.requestCells.set(transformed.id, currentCell) + } + this.requests.set(transformed.id, transformed) + void this.send({ type, data: transformed }) + if (type === 'endRequest') { + this.requests.delete(transformed.id) + this.requestCells.delete(transformed.id) + } return this } - private async healthCheck() { - const ws = await this.ws - const ping = () => { - ws.send( - JSON.stringify({ - type: 'healthcheck', - data: {} - }) - ) - } - ping() - setInterval(ping, 2000) - } + public responseRequest(id: string, response: CapturedIncomingMessage): void + public responseRequest(request: RequestDetail, response: CapturedIncomingMessage): void + public responseRequest( + idOrRequest: string | RequestDetail, + response: CapturedIncomingMessage + ): void { + if (this.disposed) return + const id = typeof idOrRequest === 'string' ? idOrRequest : idOrRequest.id + const request = requestForId( + id, + typeof idOrRequest === 'string' ? this.requests.get(id) : idOrRequest + ) + request.responseHeaders = response.headers ?? request.responseHeaders ?? {} + request.responseStatusCode = response.statusCode ?? request.responseStatusCode ?? 0 + ;(request as RequestDetail & { responseStatusText?: string }).responseStatusText = + response.statusMessage + this.requests.set(id, request) + + // responseReceived is emitted as soon as headers arrive. Body completion is + // deliberately separate so failures never produce loadingFinished. + void this.send({ type: 'responseReceived', data: request }) - public responseRequest(id: string, response: IncomingMessage) { - const responseBuffer: Buffer[] = [] - - response.on('data', (chunk: any) => { - responseBuffer.push(chunk) - }) - - response.on('end', () => { - const rawData = Buffer.concat(responseBuffer) - this.ws.then((ws) => { - ws.send( - JSON.stringify({ - type: 'responseData', - data: { - id: id, - rawData: rawData, - statusCode: response.statusCode, - headers: response.headers - } - }), - { binary: true } - ) + const chunks: Buffer[] = [] + let settled = false + + const cleanup = () => { + response.off('data', onData) + response.off('end', onEnd) + response.off('aborted', onAborted) + response.off('error', onError) + response.off('close', onClose) + this.responseCleanups.delete(cleanup) + this.requests.delete(id) + this.requestCells.delete(id) + } + const fail = (errorText: string, canceled = false) => { + if (settled || this.disposed) return + settled = true + request.requestEndTime = Date.now() / 1_000 + cleanup() + void this.send({ + type: 'requestFailed', + data: { + request, + errorText, + ...(canceled ? { canceled: true } : {}) + } }) - }) + } + const onData = (chunk: unknown) => { + if (settled) return + if (Buffer.isBuffer(chunk)) chunks.push(chunk) + else if (chunk instanceof Uint8Array) chunks.push(Buffer.from(chunk)) + else chunks.push(Buffer.from(String(chunk))) + } + const onEnd = () => { + if (settled || this.disposed) return + settled = true + const rawData = Buffer.concat(chunks) + request.requestEndTime = Date.now() / 1_000 + const data: LegacyResponseData = { + id, + rawData, + statusCode: response.statusCode ?? request.responseStatusCode ?? 0, + ...(response.statusMessage ? { statusMessage: response.statusMessage } : {}), + headers: response.headers ?? request.responseHeaders ?? {}, + ...(headerValue(response.headers ?? {}, 'content-encoding') + ? { contentEncoding: headerValue(response.headers ?? {}, 'content-encoding') } + : {}) + } + cleanup() + void this.send({ type: 'responseData', data }) + } + const onAborted = () => fail('The response was aborted.', true) + const onError = (error: unknown) => fail(error instanceof Error ? error.message : String(error)) + const onClose = () => { + if (!settled) fail('The response stream closed before completion.') + } + + response.on('data', onData) + response.once('end', onEnd) + response.once('aborted', onAborted) + response.once('error', onError) + response.once('close', onClose) + this.responseCleanups.add(cleanup) } - public async dispose() { - const ws = await this.ws - ws.removeAllListeners() - ws.terminate() - if (!this.cp) return - this.cp.removeAllListeners() - this.cp.kill() - this.cp = void 0 + public async dispose(): Promise { + if (this.disposed) return + this.disposed = true + for (const cleanup of [...this.responseCleanups]) cleanup() + this.responseCleanups.clear() + this.requests.clear() + this.requestCells.clear() + await this.bridge.dispose() } } -export { __dirname } diff --git a/packages/network-debugger/src/core/index.test.ts b/packages/network-debugger/src/core/index.test.ts index 4def64b..1eaf838 100644 --- a/packages/network-debugger/src/core/index.test.ts +++ b/packages/network-debugger/src/core/index.test.ts @@ -1,372 +1,577 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import http from 'http' import https from 'https' +import type { + AdapterProbe, + AdapterSession, + CapabilityMap, + DevtoolsTarget, + Diagnostic +} from '../adapters/types' + +const mocks = vi.hoisted(() => ({ + mainProcessConstructorCalls: [] as Record[], + mainProcessDispose: vi.fn<() => Promise>(), + proxyFetch: vi.fn(), + unsetFetch: vi.fn(), + requestProxyFactory: vi.fn(), + getProxyFactory: vi.fn(), + undiciFetchProxy: vi.fn(), + unsetUndiciFetch: vi.fn(), + generateHash: vi.fn(), + nativeProbe: vi.fn(), + nativeStart: vi.fn(), + openDevtoolsTarget: vi.fn<() => Promise>(), + sessionRecorderStart: vi.fn(), + sessionRecorderClose: vi.fn<() => Promise>(), + exportHar: vi.fn<() => Promise>() +})) -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { mainProcessConstructorCalls, mockDispose, mockSendRequest, mockSend } = vi.hoisted(() => { - return { - mainProcessConstructorCalls: [] as Record[], - mockDispose: vi.fn(), - mockSendRequest: vi.fn(), - mockSend: vi.fn() - } -}) +vi.mock('./fork', () => { + class MainProcess { + readonly ready: Promise + + constructor(readonly options: Record) { + mocks.mainProcessConstructorCalls.push(options) + const port = Number(options.serverPort) || 49_152 + const id = 'node-network-devtools-legacy-test' + const authority = `127.0.0.1:${port}` + this.ready = Promise.resolve({ + id, + title: 'Node Network Devtools (Legacy)', + type: 'node', + url: '', + webSocketDebuggerUrl: `ws://${authority}/devtools/page/${id}`, + devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?ws=${authority}/devtools/page/${id}`, + discoveryUrl: `http://${authority}/json/list` + }) + } -// Mock MainProcess 类型定义 -interface MockMainProcessInstance { - props: Record - sendRequest: () => MockMainProcessInstance - send: () => void - dispose: () => void -} + send() {} -// Mock 依赖模块 - 使用函数声明而不是类表达式 -vi.mock('./fork', () => { - // 使用函数构造器模式 - function MainProcess(this: MockMainProcessInstance, props: Record) { - this.props = props - mainProcessConstructorCalls.push(props) - const self = this - this.sendRequest = function (): MockMainProcessInstance { - mockSendRequest() - return self + sendRequest() { + return this } - this.send = function (): void { - mockSend() + + responseRequest() {} + + dispose() { + return mocks.mainProcessDispose() } - this.dispose = function (): void { - mockDispose() + + onDiagnostic() { + return () => undefined } - } - return { - __esModule: true, - MainProcess + onFailure() { + return () => undefined + } } -}) -vi.mock('./fetch', () => ({ - __esModule: true, - proxyFetch: vi.fn().mockReturnValue(() => {}) -})) + return { MainProcess } +}) +vi.mock('./fetch', () => ({ proxyFetch: mocks.proxyFetch })) vi.mock('./request', () => ({ - __esModule: true, - requestProxyFactory: vi.fn().mockReturnValue(() => {}) + requestProxyFactory: mocks.requestProxyFactory, + getProxyFactory: mocks.getProxyFactory })) - -vi.mock('./undici', () => ({ - __esModule: true, - undiciFetchProxy: vi.fn().mockReturnValue(() => {}) +vi.mock('./undici', () => ({ undiciFetchProxy: mocks.undiciFetchProxy })) +vi.mock('../utils', () => ({ generateHash: mocks.generateHash })) +vi.mock('../target/frontend-launcher', () => ({ + openDevtoolsTarget: mocks.openDevtoolsTarget })) - -vi.mock('../utils', () => ({ - __esModule: true, - generateHash: vi.fn().mockReturnValue('mock-hash-key') +vi.mock('../session', () => ({ + SessionRecorder: { start: mocks.sessionRecorderStart }, + exportHar: mocks.exportHar +})) +vi.mock('../adapters/node-native', () => ({ + NodeNativeAdapterError: class NodeNativeAdapterError extends Error { + readonly code: string + readonly hint?: string + readonly diagnostics: readonly Diagnostic[] + + constructor(diagnostic: Diagnostic, diagnostics: readonly Diagnostic[]) { + super(diagnostic.message) + this.name = 'NodeNativeAdapterError' + this.code = diagnostic.code + this.hint = diagnostic.hint + this.diagnostics = diagnostics + } + }, + NodeNativeAdapter: class { + readonly kind = 'native' as const + probe = mocks.nativeProbe + start = mocks.nativeStart + } })) -// 导入被测模块和 mock 模块 import { register } from './index' -import { proxyFetch } from './fetch' -import { requestProxyFactory } from './request' -import { undiciFetchProxy } from './undici' -import { generateHash } from '../utils' +import { disposeActiveRegistration, RuntimeRegistrationError } from '../runtime/controller' +import { LEGACY_CAPABILITIES } from '../adapters/legacy' + +const NATIVE_CAPABILITIES: CapabilityMap = Object.freeze({ + http: true, + https: true, + fetch: true, + http2: true, + responseBody: true, + requestBody: true, + websocketLifecycle: true, + websocketFrames: true, + sseMessages: false, + initiator: true +}) + +const EMPTY_CAPABILITIES: CapabilityMap = Object.freeze({ + http: false, + https: false, + fetch: false, + http2: false, + responseBody: false, + requestBody: false, + websocketLifecycle: false, + websocketFrames: false, + sseMessages: false, + initiator: false +}) + +const NATIVE_TARGET: DevtoolsTarget = Object.freeze({ + id: 'native-target', + title: 'Node.js', + type: 'node', + url: 'file:///fixture.js', + webSocketDebuggerUrl: 'ws://127.0.0.1:9229/native-target', + devtoolsFrontendUrl: 'devtools://native-target', + discoveryUrl: 'http://127.0.0.1:9229/json/list' +}) + +const nativeUnavailableDiagnostic: Diagnostic = Object.freeze({ + code: 'NND_NATIVE_FLAG_REQUIRED', + level: 'error', + message: 'Native network inspection requires --experimental-network-inspection.', + hint: 'Restart with: node --inspect=0 --experimental-network-inspection ' +}) + +const nativeUnavailableProbe = (): AdapterProbe => ({ + kind: 'native', + available: false, + autoSelectable: false, + capabilities: EMPTY_CAPABILITIES, + diagnostics: [nativeUnavailableDiagnostic] +}) -describe('core/index.ts', () => { +const nativeAvailableProbe = (): AdapterProbe => ({ + kind: 'native', + available: true, + autoSelectable: true, + capabilities: NATIVE_CAPABILITIES, + diagnostics: [] +}) + +const nativeSession = (dispose = vi.fn<() => Promise>()): AdapterSession => ({ + kind: 'native', + capabilities: NATIVE_CAPABILITIES, + target: NATIVE_TARGET, + diagnostics: [], + dispose +}) + +describe('core register compatibility API', () => { let originalHttpRequest: typeof http.request let originalHttpsRequest: typeof https.request + let originalHttpGet: typeof http.get + let originalHttpsGet: typeof https.get - beforeEach(() => { + beforeEach(async () => { + await disposeActiveRegistration() vi.clearAllMocks() - mainProcessConstructorCalls.length = 0 - // 保存原始的 request 方法 + mocks.mainProcessConstructorCalls.length = 0 originalHttpRequest = http.request originalHttpsRequest = https.request + originalHttpGet = http.get + originalHttpsGet = https.get + + mocks.mainProcessDispose.mockResolvedValue(undefined) + mocks.proxyFetch.mockReturnValue(mocks.unsetFetch) + mocks.requestProxyFactory.mockImplementation(() => vi.fn()) + mocks.getProxyFactory.mockImplementation(() => vi.fn()) + mocks.undiciFetchProxy.mockReturnValue(mocks.unsetUndiciFetch) + mocks.generateHash.mockReturnValue('mock-hash-key') + mocks.nativeProbe.mockImplementation(nativeUnavailableProbe) + mocks.nativeStart.mockImplementation(async () => nativeSession()) + mocks.openDevtoolsTarget.mockResolvedValue(undefined) + mocks.sessionRecorderClose.mockResolvedValue(undefined) + mocks.sessionRecorderStart.mockResolvedValue({ + directory: '/recordings/session-1', + getManifest: () => ({ sessionId: 'session-1' }), + close: mocks.sessionRecorderClose + }) + mocks.exportHar.mockResolvedValue({}) }) - afterEach(() => { - // 恢复原始的 request 方法 + afterEach(async () => { + await disposeActiveRegistration() http.request = originalHttpRequest https.request = originalHttpsRequest + http.get = originalHttpGet + https.get = originalHttpsGet }) - describe('register 函数', () => { - describe('默认配置', () => { - test('不传参数时使用默认配置', () => { - const unregister = register() - - expect(mainProcessConstructorCalls).toHaveLength(1) - expect(mainProcessConstructorCalls[0]).toEqual({ - port: 5270, - serverPort: 5271, - autoOpenDevtool: true, - key: 'mock-hash-key' - }) - - // 清理 - if (unregister) unregister() - }) - - test('默认拦截 fetch', () => { - const unregister = register() - - expect(proxyFetch).toHaveBeenCalled() - - if (unregister) unregister() - }) - - test('默认拦截 http/https', () => { - const unregister = register() - - expect(requestProxyFactory).toHaveBeenCalledTimes(2) - - if (unregister) unregister() - }) - - test('默认不拦截 undici', () => { - const unregister = register() - - expect(undiciFetchProxy).not.toHaveBeenCalled() - - if (unregister) unregister() - }) + test('register returns a callable handle with an observable ready lifecycle', async () => { + const handle = register({ mode: 'legacy' }) + + expect(typeof handle).toBe('function') + expect(handle.ready).toBeInstanceOf(Promise) + expect(handle.status()).toEqual({ state: 'starting' }) + expect(typeof handle.dispose).toBe('function') + expect(typeof handle.openDevtools).toBe('function') + expect(typeof handle.on).toBe('function') + + const ready = await handle.ready + + expect(handle.status()).toEqual({ state: 'ready', mode: 'legacy' }) + expect(ready).toEqual({ + mode: 'legacy', + target: { + id: 'node-network-devtools-legacy-test', + title: 'Node Network Devtools (Legacy)', + type: 'node', + url: '', + webSocketDebuggerUrl: + 'ws://127.0.0.1:49152/devtools/page/node-network-devtools-legacy-test', + devtoolsFrontendUrl: + 'devtools://devtools/bundled/js_app.html?ws=127.0.0.1:49152/devtools/page/node-network-devtools-legacy-test', + discoveryUrl: 'http://127.0.0.1:49152/json/list' + }, + capabilities: LEGACY_CAPABILITIES, + diagnostics: [], + fallbackReason: undefined }) + }) - describe('自定义配置', () => { - test('自定义端口配置', () => { - const unregister = register({ - port: 8080, - serverPort: 8081 - }) - - expect(mainProcessConstructorCalls).toHaveLength(1) - expect(mainProcessConstructorCalls[0]).toMatchObject({ - port: 8080, - serverPort: 8081 - }) - - if (unregister) unregister() - }) + test('ready exposes the native mode, target, and capabilities', async () => { + const disposeNative = vi.fn<() => Promise>().mockResolvedValue(undefined) + const session = nativeSession(disposeNative) + mocks.nativeProbe.mockImplementation(nativeAvailableProbe) + mocks.nativeStart.mockResolvedValue(session) - test('禁用自动打开 DevTools', () => { - const unregister = register({ - autoOpenDevtool: false - }) + const handle = register({ + mode: 'native', + requiredCapabilities: ['http', 'fetch', 'http2'] + }) - expect(mainProcessConstructorCalls).toHaveLength(1) - expect(mainProcessConstructorCalls[0]).toMatchObject({ - autoOpenDevtool: false - }) + await expect(handle.ready).resolves.toEqual({ + mode: 'native', + target: NATIVE_TARGET, + capabilities: NATIVE_CAPABILITIES, + diagnostics: [], + fallbackReason: undefined + }) + expect(mocks.nativeStart).toHaveBeenCalledWith({ + requiredCapabilities: ['http', 'fetch', 'http2'], + inspector: { host: '127.0.0.1', port: 0 } + }) + expect(mocks.mainProcessConstructorCalls).toHaveLength(0) - if (unregister) unregister() - }) + await handle.dispose() + expect(disposeNative).toHaveBeenCalledOnce() + }) - test('禁用 fetch 拦截', () => { - const unregister = register({ - intercept: { - fetch: false - } - }) + test('auto exposes a structured fallback when native is unavailable', async () => { + const handle = register({ + mode: 'auto', + requiredCapabilities: ['fetch', 'responseBody'] + }) - expect(proxyFetch).not.toHaveBeenCalled() + const ready = await handle.ready + + expect(ready.mode).toBe('legacy') + expect(ready.capabilities).toEqual(LEGACY_CAPABILITIES) + expect(ready.fallbackReason).toEqual({ + code: 'NND_AUTO_FALLBACK', + level: 'warn', + message: 'Native adapter cannot satisfy this selection; using legacy adapter.', + hint: 'Use mode "native" to fail instead of falling back.', + details: { + from: 'native', + to: 'legacy', + reason: 'unavailable', + requiredCapabilities: ['fetch', 'responseBody'], + diagnosticCodes: ['NND_NATIVE_FLAG_REQUIRED'], + diagnostics: [nativeUnavailableDiagnostic] + } + }) + expect(ready.diagnostics).toEqual([ready.fallbackReason]) + expect(mocks.nativeStart).not.toHaveBeenCalled() + }) - if (unregister) unregister() - }) + test('forced native publishes the actionable Native error and releases active state', async () => { + const failed = register({ mode: 'native' }) - test('禁用 http/https 拦截', () => { - const unregister = register({ - intercept: { - normal: false - } - }) + await expect(failed.ready).rejects.toMatchObject({ + name: 'NodeNativeAdapterError', + code: 'NND_NATIVE_FLAG_REQUIRED', + message: 'Native network inspection requires --experimental-network-inspection.', + hint: 'Restart with: node --inspect=0 --experimental-network-inspection ' + }) + expect(failed.status()).toMatchObject({ state: 'failed' }) - expect(requestProxyFactory).not.toHaveBeenCalled() + await failed.dispose() + expect(failed.status()).toEqual({ state: 'disposed', mode: undefined }) - if (unregister) unregister() - }) + const replacement = register({ mode: 'legacy' }) + await expect(replacement.ready).resolves.toMatchObject({ mode: 'legacy' }) + }) - test('启用 undici fetch 拦截', () => { - const unregister = register({ - intercept: { - undici: { - fetch: true + test('rejects Native plus Mock synchronously with a stable capability conflict', () => { + expect(() => + register({ + mode: 'native', + legacy: { + mock: [ + { + match: { url: 'https://example.test/*' }, + response: { status: 200, body: 'mocked' } } - } - }) - - expect(undiciFetchProxy).toHaveBeenCalled() - - if (unregister) unregister() + ] + } }) - - test('undici 配置为 false 时不拦截', () => { - const unregister = register({ - intercept: { - undici: false - } - }) - - expect(undiciFetchProxy).not.toHaveBeenCalled() - - if (unregister) unregister() + ).toThrowError( + expect.objectContaining({ + name: 'RuntimeRegistrationError', + code: 'NND_NATIVE_MOCK_CONFLICT', + message: 'Request/response mocking is available only with the Legacy backend.' }) + ) + expect(mocks.nativeStart).not.toHaveBeenCalled() + expect(mocks.mainProcessConstructorCalls).toHaveLength(0) + }) - test('undici.fetch 为 false 时不拦截', () => { - const unregister = register({ - intercept: { - undici: { - fetch: false - } - } - }) - - expect(undiciFetchProxy).not.toHaveBeenCalled() - - if (unregister) unregister() - }) + test('Auto selects Legacy and exposes a structured reason when Mock is configured', async () => { + mocks.nativeProbe.mockImplementation(nativeAvailableProbe) + const mock = [ + { + id: 'fixture', + match: { url: 'https://example.test/*' }, + response: { status: 200, body: 'mocked' } + } + ] as const + + const handle = register({ mode: 'auto', legacy: { mock } }) + const ready = await handle.ready + + expect(ready.mode).toBe('legacy') + expect(ready.fallbackReason).toEqual({ + code: 'NND_AUTO_LEGACY_MOCK_REQUIRED', + level: 'info', + message: 'Auto selected Legacy because request/response mocking was configured.', + hint: 'Remove legacy.mock to allow Auto to select the Native backend.' }) + expect(ready.diagnostics).toContainEqual(ready.fallbackReason) + expect(mocks.nativeStart).not.toHaveBeenCalled() + expect(mocks.proxyFetch).toHaveBeenCalledWith(expect.anything(), mock) + }) - describe('generateHash 调用', () => { - test('使用配置生成 hash key', () => { - const unregister = register({ - port: 3000, - serverPort: 3001, - autoOpenDevtool: false - }) - - expect(generateHash).toHaveBeenCalledWith( - JSON.stringify({ - port: 3000, - serverPort: 3001, - autoOpenDevtool: false - }) - ) - - if (unregister) unregister() - }) + test('records a Session for either backend and exports HAR before backend disposal', async () => { + const handle = register({ + mode: 'legacy', + session: { + directory: '/recordings/session-1', + bodyCommandTimeoutMs: 2500, + har: '/recordings/session-1.har' + } }) - describe('http/https 请求代理', () => { - test('http.request 被替换为代理函数', () => { - const originalRequest = http.request - const unregister = register() - - expect(http.request).not.toBe(originalRequest) - expect(requestProxyFactory).toHaveBeenCalledWith(originalRequest, false, expect.anything()) - - if (unregister) unregister() - }) - - test('https.request 被替换为代理函数', () => { - const originalRequest = https.request - const unregister = register() - - expect(https.request).not.toBe(originalRequest) - expect(requestProxyFactory).toHaveBeenCalledWith(originalRequest, true, expect.anything()) - - if (unregister) unregister() - }) + const ready = await handle.ready + expect(mocks.sessionRecorderStart).toHaveBeenCalledWith({ + directory: '/recordings/session-1', + target: ready.target, + bodyCommandTimeoutMs: 2500 }) + expect(ready.session).toEqual({ + directory: '/recordings/session-1', + sessionId: 'session-1' + }) + expect(ready.diagnostics).toContainEqual( + expect.objectContaining({ code: 'NND_SESSION_RECORDING_STARTED' }) + ) + + await handle.dispose() + expect(mocks.sessionRecorderClose).toHaveBeenCalledOnce() + expect(mocks.exportHar).toHaveBeenCalledWith( + '/recordings/session-1', + '/recordings/session-1.har' + ) + expect(mocks.sessionRecorderClose.mock.invocationCallOrder[0]).toBeLessThan( + mocks.exportHar.mock.invocationCallOrder[0] + ) + expect(mocks.exportHar.mock.invocationCallOrder[0]).toBeLessThan( + mocks.mainProcessDispose.mock.invocationCallOrder[0] + ) + }) - describe('unregister 函数', () => { - test('返回 unregister 函数', () => { - const unregister = register() - - expect(typeof unregister).toBe('function') - - if (unregister) unregister() - }) - - test('调用 unregister 后恢复 http.request', () => { - const originalRequest = http.request - const unregister = register() + test('releases the backend when Session startup fails', async () => { + mocks.sessionRecorderStart.mockRejectedValueOnce(new Error('recording directory exists')) + const handle = register({ + mode: 'legacy', + session: { directory: '/recordings/existing' } + }) - expect(http.request).not.toBe(originalRequest) + await expect(handle.ready).rejects.toThrow('recording directory exists') + expect(mocks.mainProcessDispose).toHaveBeenCalledOnce() + }) - if (unregister) unregister() + test('explicit legacy activates old capture options and restores every patch', async () => { + const handle = register({ + mode: 'legacy', + port: 8080, + serverPort: 8081, + autoOpenDevtool: true, + intercept: { + fetch: true, + normal: true, + undici: { fetch: true } + } + }) - expect(http.request).toBe(originalRequest) - }) + const ready = await handle.ready + + expect(new URL(ready.target.webSocketDebuggerUrl).port).toBe('8081') + expect(ready.target.discoveryUrl).toBe('http://127.0.0.1:8081/json/list') + + expect(mocks.generateHash).toHaveBeenCalledWith( + JSON.stringify({ port: 8080, serverPort: 8081, autoOpenDevtool: false }) + ) + expect(mocks.mainProcessConstructorCalls).toEqual([ + { + port: 8080, + serverPort: 8081, + autoOpenDevtool: false, + key: 'mock-hash-key' + } + ]) + expect(mocks.proxyFetch).toHaveBeenCalledOnce() + expect(mocks.requestProxyFactory).toHaveBeenCalledTimes(2) + expect(mocks.requestProxyFactory).toHaveBeenNthCalledWith( + 1, + originalHttpRequest, + false, + expect.anything() + ) + expect(mocks.requestProxyFactory).toHaveBeenNthCalledWith( + 2, + originalHttpsRequest, + true, + expect.anything() + ) + expect(mocks.undiciFetchProxy).toHaveBeenCalledOnce() + expect(http.request).not.toBe(originalHttpRequest) + expect(https.request).not.toBe(originalHttpsRequest) + expect(mocks.openDevtoolsTarget).toHaveBeenCalledOnce() + expect(mocks.openDevtoolsTarget).toHaveBeenCalledWith(ready.target) + expect(ready.diagnostics).toContainEqual({ + code: 'NND_LEGACY_OPTIONS_DEPRECATED', + level: 'warn', + message: 'Top-level Legacy options are deprecated.', + hint: 'Move capture and port settings under "legacy", and browser behavior under "devtools".' + }) - test('调用 unregister 后恢复 https.request', () => { - const originalRequest = https.request - const unregister = register() + await handle.dispose() - expect(https.request).not.toBe(originalRequest) + expect(mocks.unsetFetch).toHaveBeenCalledOnce() + expect(mocks.unsetUndiciFetch).toHaveBeenCalledOnce() + expect(mocks.mainProcessDispose).toHaveBeenCalledOnce() + expect(http.request).toBe(originalHttpRequest) + expect(https.request).toBe(originalHttpsRequest) + }) - if (unregister) unregister() + test('legacy namespaced options also activate configured interceptors', async () => { + const handle = register({ + mode: 'legacy', + legacy: { + port: 7070, + serverPort: 7071, + intercept: { + fetch: false, + normal: false, + undici: { fetch: true } + } + } + }) - expect(https.request).toBe(originalRequest) - }) + await handle.ready - test('调用 unregister 后调用 MainProcess.dispose', () => { - mockDispose.mockClear() + expect(mocks.mainProcessConstructorCalls[0]).toMatchObject({ + port: 7070, + serverPort: 7071, + autoOpenDevtool: false + }) + expect(mocks.proxyFetch).not.toHaveBeenCalled() + expect(mocks.requestProxyFactory).not.toHaveBeenCalled() + expect(mocks.undiciFetchProxy).toHaveBeenCalledOnce() + expect(http.request).toBe(originalHttpRequest) + expect(https.request).toBe(originalHttpsRequest) + }) - const unregister = register() + test('the same normalized configuration returns one idempotent handle', async () => { + const first = register({ + mode: 'legacy', + requiredCapabilities: ['responseBody', 'fetch'], + legacy: { port: 6000, serverPort: 6001 } + }) + const second = register({ + mode: 'legacy', + requiredCapabilities: ['fetch', 'responseBody'], + legacy: { port: 6000, serverPort: 6001 } + }) - if (unregister) unregister() + expect(second).toBe(first) + await first.ready + expect(mocks.mainProcessConstructorCalls).toHaveLength(1) + expect(mocks.proxyFetch).toHaveBeenCalledOnce() + }) - expect(mockDispose).toHaveBeenCalled() - }) + test('a conflicting active configuration fails synchronously with a stable error', async () => { + const active = register({ mode: 'legacy', legacy: { port: 6000 } }) - test('禁用 fetch 拦截时 unregister 不调用 fetch 清理函数', () => { - const unregister = register({ - intercept: { - fetch: false - } - }) - - // 不应该抛出错误 - expect(() => { - if (unregister) unregister() - }).not.toThrow() + expect(() => register({ mode: 'legacy', legacy: { port: 6001 } })).toThrowError( + expect.objectContaining({ + name: 'RuntimeRegistrationError', + code: 'NND_ALREADY_REGISTERED', + message: 'Node Network Devtools is already registered with a different configuration.' }) + ) + expect(() => register({ mode: 'native' })).toThrow(RuntimeRegistrationError) - test('禁用 normal 拦截时 unregister 不恢复 http/https', () => { - const originalHttpRequest = http.request - const originalHttpsRequest = https.request - - const unregister = register({ - intercept: { - normal: false - } - }) - - // http/https.request 应该保持不变 - expect(http.request).toBe(originalHttpRequest) - expect(https.request).toBe(originalHttpsRequest) - - if (unregister) unregister() - }) + await active.ready + expect(mocks.mainProcessConstructorCalls).toHaveLength(1) + }) - test('启用 undici 拦截时 unregister 调用 undici 清理函数', () => { - const mockUnsetUndici = vi.fn() - vi.mocked(undiciFetchProxy).mockReturnValue(mockUnsetUndici) + test('callable cleanup and async dispose share one idempotent cleanup', async () => { + const handle = register({ mode: 'legacy' }) + await handle.ready - const unregister = register({ - intercept: { - undici: { - fetch: true - } - } - }) + expect(handle()).toBeUndefined() + await Promise.all([handle.dispose(), handle.dispose()]) - if (unregister) unregister() + expect(mocks.unsetFetch).toHaveBeenCalledOnce() + expect(mocks.mainProcessDispose).toHaveBeenCalledOnce() + expect(handle.status()).toEqual({ state: 'disposed', mode: 'legacy' }) - expect(mockUnsetUndici).toHaveBeenCalled() - }) - }) + await handle.dispose() + expect(mocks.mainProcessDispose).toHaveBeenCalledOnce() + }) - describe('多次注册', () => { - test('多次注册创建多个 MainProcess 实例', () => { - const unregister1 = register() - const unregister2 = register() + test('browser opening defaults to false and remains explicitly callable', async () => { + const handle = register({ mode: 'legacy' }) + const ready = await handle.ready - expect(mainProcessConstructorCalls).toHaveLength(2) + expect(mocks.mainProcessConstructorCalls[0]).toMatchObject({ autoOpenDevtool: false }) + expect(mocks.openDevtoolsTarget).not.toHaveBeenCalled() - if (unregister1) unregister1() - if (unregister2) unregister2() - }) - }) + await handle.openDevtools() + expect(mocks.openDevtoolsTarget).toHaveBeenCalledOnce() + expect(mocks.openDevtoolsTarget).toHaveBeenCalledWith(ready.target) }) }) diff --git a/packages/network-debugger/src/core/index.ts b/packages/network-debugger/src/core/index.ts index a59ce48..1f51ef3 100644 --- a/packages/network-debugger/src/core/index.ts +++ b/packages/network-debugger/src/core/index.ts @@ -1,69 +1,21 @@ -import http from 'http' -import https from 'https' -import { requestProxyFactory } from './request' -import { MainProcess } from './fork' -import { proxyFetch } from './fetch' -import { PORT, SERVER_PORT } from '../common' - -import { RegisterOptions } from '../common' -import { generateHash } from '../utils' -import { undiciFetchProxy } from './undici' - -export function register(props?: RegisterOptions) { - const { - port = PORT, - serverPort = SERVER_PORT, - autoOpenDevtool = true, - intercept = {} - } = props || {} - - const { - fetch: isInterceptFetch = true, - normal: isInterceptNormal = true, - undici: isInterceptUndici = false - } = intercept - - const interceptUndiciFetch = isInterceptUndici && isInterceptUndici.fetch - - const key = generateHash(JSON.stringify({ port, serverPort, autoOpenDevtool })) - const mainProcess = new MainProcess({ - port, - serverPort, - autoOpenDevtool, - key - }) - - // global fetch - const unsetFetchProxy = isInterceptFetch ? proxyFetch(mainProcess) : void 0 - - // http/https - const originAgentRequests = new WeakMap() - const agents = [http, https] - if (isInterceptNormal) { - agents.forEach((agent) => { - originAgentRequests.set(agent, agent.request) - const actualRequestHandlerFn = agent.request - agent.request = requestProxyFactory(actualRequestHandlerFn, agent === https, mainProcess) - }) - } - - // undici - // undici fetch - const unsetUndiciFetch = interceptUndiciFetch ? undiciFetchProxy(mainProcess) : void 0 - - return () => { - unsetFetchProxy && unsetFetchProxy() - if (isInterceptNormal) { - agents.forEach((agent) => { - agent.request = originAgentRequests.get(agent) - originAgentRequests.delete(agent) - }) - } - - unsetUndiciFetch && unsetUndiciFetch() - - mainProcess.dispose() - } -} - +export { register } from '../runtime/controller' +export type { + ReadyInfo, + RegistrationEvent, + RegistrationHandle, + RegistrationState, + RegistrationStatus +} from '../runtime/registration' +export type { + AdapterKind, + AdapterMode, + CapabilityMap, + DevtoolsTarget, + Diagnostic, + NetworkCapability +} from '../adapters/types' +export type { InterceptOptions, RegisterOptions } from '../common' +export * from '../mock' +export * from '../replay' +export * from '../session' export * from './hooks' diff --git a/packages/network-debugger/src/core/request.test.ts b/packages/network-debugger/src/core/request.test.ts index 2644339..4895e89 100644 --- a/packages/network-debugger/src/core/request.test.ts +++ b/packages/network-debugger/src/core/request.test.ts @@ -1,1009 +1,526 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import { EventEmitter } from 'events' -import type { ClientRequest, IncomingMessage, RequestOptions } from 'http' -import type { Socket } from 'net' -import { RequestDetail } from '../common' -import { requestProxyFactory } from './request' +import { EventEmitter } from 'node:events' +import type { ClientRequest, IncomingMessage, RequestOptions } from 'node:http' +import type { Socket } from 'node:net' +import { deserialize, serialize } from 'node:v8' +import { beforeEach, describe, expect, test, vi } from 'vitest' import type { MainProcess } from './fork' +import { getProxyFactory, requestProxyFactory, type RequestFn } from './request' +import { withoutLegacyCapture } from './capture-scope' +import PerMessageDeflate from './ws/permessage-deflate' + +interface JournalEntry { + transport: 'request' | 'event' | 'response' + type: string + data: any +} -// Mock MainProcess 接口 -interface MockMainProcess { - sendRequest: ReturnType - send: ReturnType - responseRequest: ReturnType +interface MockClientRequest extends ClientRequest { + protocol: string + host: string + destroyed: boolean } -// 创建 mock MainProcess -function createMockMainProcess(): { - mockMainProcess: MockMainProcess - mockSendRequest: ReturnType - mockSend: ReturnType - mockResponseRequest: ReturnType -} { - const mockSendRequest = vi.fn().mockReturnThis() - const mockSend = vi.fn().mockResolvedValue(undefined) - const mockResponseRequest = vi.fn() +function snapshot(value: T): T { + return deserialize(serialize(value)) as T +} - return { - mockMainProcess: { - sendRequest: mockSendRequest, - send: mockSend, - responseRequest: mockResponseRequest - }, - mockSendRequest, - mockSend, - mockResponseRequest - } +function createMainProcess() { + const journal: JournalEntry[] = [] + const mainProcess: Record = {} + mainProcess.sendRequest = vi.fn((type: string, data: unknown) => { + journal.push({ transport: 'request', type, data: snapshot(data) }) + return mainProcess + }) + mainProcess.send = vi.fn(async (event: { type: string; data: unknown }) => { + journal.push({ transport: 'event', type: event.type, data: snapshot(event.data) }) + }) + mainProcess.responseRequest = vi.fn((request: unknown, response: IncomingMessage) => { + journal.push({ + transport: 'response', + type: 'responseRequest', + data: { request: snapshot(request), response } + }) + }) + return { journal, mainProcess: mainProcess as MainProcess } } -// 创建 Mock ClientRequest - 返回具有必要属性的对象 -function createMockClientRequest() { - const emitter = new EventEmitter() - const writeFn = vi.fn().mockReturnValue(true) - const setHeaderFn = vi.fn() - - // 将 EventEmitter 方法和 mock 方法合并 - const mockRequest = { - ...emitter, - on: emitter.on.bind(emitter), - emit: emitter.emit.bind(emitter), - write: writeFn, - end: vi.fn(), - setHeader: setHeaderFn, - getHeader: vi.fn(), - removeHeader: vi.fn(), - abort: vi.fn(), - destroyed: false +function createClientRequest( + overrides: Partial<{ + protocol: string + host: string + path: string + method: string + headers: Record + }> = {} +): MockClientRequest { + const request = new EventEmitter() as MockClientRequest + const headers: Record = { + host: overrides.host ?? 'example.test', + ...(overrides.headers ?? {}) } + request.protocol = overrides.protocol ?? 'http:' + request.host = overrides.host ?? 'example.test' + request.path = overrides.path ?? '/' + request.method = overrides.method ?? 'GET' + request.destroyed = false + request.getHeader = vi.fn((name: string) => { + const key = Object.keys(headers).find( + (candidate) => candidate.toLowerCase() === name.toLowerCase() + ) + return key ? headers[key] : undefined + }) + request.getHeaders = vi.fn(() => ({ ...headers })) + request.setHeader = vi.fn((name: string, value: string | string[] | number) => { + const existing = Object.keys(headers).find( + (candidate) => candidate.toLowerCase() === name.toLowerCase() + ) + if (existing) delete headers[existing] + headers[name] = value + return request + }) as ClientRequest['setHeader'] + request.removeHeader = vi.fn((name: string) => { + const existing = Object.keys(headers).find( + (candidate) => candidate.toLowerCase() === name.toLowerCase() + ) + if (existing) delete headers[existing] + }) + request.write = vi.fn(() => true) as ClientRequest['write'] + request.end = vi.fn(() => request) as ClientRequest['end'] + request.abort = vi.fn() + return request +} - return mockRequest +function createResponse(overrides: Partial = {}): IncomingMessage { + const response = new EventEmitter() as IncomingMessage + response.statusCode = overrides.statusCode ?? 200 + response.statusMessage = overrides.statusMessage ?? 'OK' + response.headers = overrides.headers ?? { 'content-type': 'application/json' } + response.rawHeaders = overrides.rawHeaders ?? ['Content-Type', 'application/json'] + response.httpVersion = overrides.httpVersion ?? '1.1' + return response } -// 创建 Mock IncomingMessage -function createMockIncomingMessage( - options: { - statusCode?: number - headers?: Record - } = {} -) { - const { statusCode = 200, headers = {} } = options - const emitter = new EventEmitter() - - const mockResponse = { - ...emitter, - on: emitter.on.bind(emitter), - emit: emitter.emit.bind(emitter), - statusCode, - headers, - httpVersion: '1.1', - complete: true, - rawHeaders: [] as string[], - trailers: {}, - rawTrailers: [] as string[] - } +function createSocket(): Socket { + const socket = new EventEmitter() as Socket + socket.write = vi.fn(() => true) as Socket['write'] + socket.end = vi.fn(() => socket) as Socket['end'] + socket.destroy = vi.fn(() => socket) as Socket['destroy'] + return socket +} - return mockResponse +function createActualRequest(request: ClientRequest) { + let responseCallback: ((response: IncomingMessage) => void) | undefined + const actualRequest = vi.fn((...args: unknown[]) => { + const candidate = args.at(-1) + if (typeof candidate === 'function') { + responseCallback = candidate as (response: IncomingMessage) => void + } + return request + }) as unknown as RequestFn + return { + actualRequest, + respond(response: IncomingMessage) { + expect(responseCallback).toBeTypeOf('function') + responseCallback!(response) + } + } } -// 创建 Mock Socket -function createMockSocket() { - const emitter = new EventEmitter() - const writeFn = vi.fn().mockReturnValue(true) - const readFn = vi.fn().mockReturnValue(null) - - const mockSocket = { - ...emitter, - on: emitter.on.bind(emitter), - emit: emitter.emit.bind(emitter), - addListener: emitter.addListener.bind(emitter), - write: writeFn, - end: vi.fn(), - destroy: vi.fn(), - read: readFn, - destroyed: false, - readable: true, - writable: true +function websocketFrame( + payload: Buffer, + opcode: 1 | 2, + masked: boolean, + compressed = false +): Buffer { + if (payload.length >= 126) throw new Error('test helper only supports short frames') + const first = 0x80 | (compressed ? 0x40 : 0) | opcode + if (!masked) return Buffer.concat([Buffer.from([first, payload.length]), payload]) + + const mask = Buffer.from([0x11, 0x22, 0x33, 0x44]) + const encoded = Buffer.alloc(payload.length) + for (let index = 0; index < payload.length; index += 1) { + encoded[index] = payload[index] ^ mask[index % mask.length] } + return Buffer.concat([Buffer.from([first, 0x80 | payload.length]), mask, encoded]) +} - return mockSocket +function compress(extension: PerMessageDeflate, payload: Buffer): Promise { + return new Promise((resolve, reject) => { + extension.compress(payload, true, (error, result) => { + if (error) reject(error) + else if (!result) reject(new Error('permessage-deflate returned no payload')) + else resolve(result) + }) + }) } -describe('core/request.ts', () => { - beforeEach(() => { - vi.clearAllMocks() +describe('http request capture', () => { + beforeEach(() => vi.clearAllMocks()) + + test('bypasses internal debugger transports without emitting capture events', () => { + const request = createClientRequest({ path: '/devtools/page/internal' }) + const { actualRequest } = createActualRequest(request) + const { journal, mainProcess } = createMainProcess() + const callback = vi.fn() + const proxy = requestProxyFactory.call(undefined, actualRequest, false, mainProcess) + + const returned = withoutLegacyCapture(() => + proxy('http://127.0.0.1:43100/devtools/page/internal', callback) + ) + + expect(returned).toBe(request) + expect(actualRequest).toHaveBeenCalledWith( + 'http://127.0.0.1:43100/devtools/page/internal', + callback + ) + expect(journal).toEqual([]) }) - afterEach(() => { - vi.restoreAllMocks() + test('uses the actual ClientRequest origin, path, method, headers, and seconds timestamp', () => { + const request = createClientRequest({ + protocol: 'http:', + host: '127.0.0.1:43891', + path: '/actual?query=1', + method: 'POST', + headers: { 'x-runtime': 'yes' } + }) + const { actualRequest } = createActualRequest(request) + const { journal, mainProcess } = createMainProcess() + const before = Date.now() / 1000 + + requestProxyFactory.call( + undefined, + actualRequest, + false, + mainProcess + )('http://placeholder.invalid/stale', { method: 'GET' }) + const after = Date.now() / 1000 + + const detail = journal[0].data + expect(detail).toMatchObject({ + url: 'http://127.0.0.1:43891/actual?query=1', + method: 'POST', + requestHeaders: { host: '127.0.0.1:43891', 'x-runtime': 'yes' } + }) + expect(detail.requestStartTime).toBeGreaterThanOrEqual(before) + expect(detail.requestStartTime).toBeLessThanOrEqual(after) + expect(detail.requestStartTime).toBeLessThan(10_000_000_000) + expect(actualRequest).toHaveBeenCalledWith( + 'http://placeholder.invalid/stale', + { method: 'GET' }, + expect.any(Function) + ) }) - describe('requestProxyFactory 函数', () => { - describe('参数解析', () => { - test('处理字符串 URL 参数 (url, options, callback)', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const callback = vi.fn() - proxyFn('http://example.com/api', { method: 'GET' }, callback) - - // 验证 initRequest 被调用 - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'http://example.com/api', - method: 'GET' - }) - ) - }) - - test('处理 URL 对象参数', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const url = new URL('http://example.com/api') - proxyFn(url, { method: 'POST' }) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'http://example.com/api', - method: 'POST' - }) - ) - }) - - test('处理 RequestOptions 参数 (options, callback)', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST', - headers: { 'Content-Type': 'application/json' } - } - proxyFn(options) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'http://example.com/api', - method: 'POST' - }) - ) - }) - - test('处理 HTTPS 请求', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - true, // isHttps = true - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'https://example.com/api' - }) - ) - }) - - test('使用 host 替代 hostname', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - host: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: 'http://example.com/api' - }) - ) - }) + test('registers lazily once at end with every written body chunk and late headers', () => { + const request = createClientRequest({ method: 'POST', path: '/upload' }) + const originalWrite = request.write + const originalEnd = request.end + const { actualRequest } = createActualRequest(request) + const { journal, mainProcess } = createMainProcess() + const captured = requestProxyFactory.call( + undefined, + actualRequest, + false, + mainProcess + )({ + hostname: 'example.test', + path: '/upload', + method: 'POST' }) - describe('请求头处理', () => { - test('记录请求头', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer token' - } - } - proxyFn(options) - - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - requestHeaders: { - 'Content-Type': 'application/json', - Authorization: 'Bearer token' - } - }) - ) - }) - - test('setHeader 方法代理 - 单个值', () => { - const mockRequest = createMockClientRequest() - const originalSetHeader = mockRequest.setHeader - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - // 需要传递 headers 以初始化 requestHeaders - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST', - headers: {} - } - const request = proxyFn(options) - - // 调用代理后的 setHeader - request.setHeader('X-Custom-Header', 'custom-value') - - // 验证原始 setHeader 被调用 - expect(originalSetHeader).toHaveBeenCalledWith('X-Custom-Header', 'custom-value') - }) - - test('setHeader 方法代理 - 数组值', () => { - const mockRequest = createMockClientRequest() - const originalSetHeader = mockRequest.setHeader - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - // 需要传递 headers 以初始化 requestHeaders - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST', - headers: {} - } - const request = proxyFn(options) - - // 调用代理后的 setHeader,传入数组 - request.setHeader('Set-Cookie', ['cookie1=value1', 'cookie2=value2']) - - // 验证原始 setHeader 被调用 - expect(originalSetHeader).toHaveBeenCalledWith('Set-Cookie', [ - 'cookie1=value1', - 'cookie2=value2' - ]) - }) + captured.write('first-', 'utf8') + captured.write(Buffer.from('second-')) + captured.setHeader('X-Late', 'visible') + expect(journal.map(({ type }) => type)).toEqual(['initRequest']) + + captured.end('third') + captured.end() + + expect(journal.map(({ type }) => type)).toEqual(['initRequest', 'registerRequest']) + const registered = journal[1].data + expect(Buffer.from(registered.requestData).toString()).toBe('first-second-third') + expect(registered.requestHeaders).toMatchObject({ 'X-Late': 'visible' }) + expect(originalWrite).toHaveBeenCalledTimes(2) + expect(originalEnd).toHaveBeenCalledTimes(2) + }) + + test('registers before delegating an early response and preserves response metadata', () => { + const request = createClientRequest({ path: '/resource' }) + const harness = createActualRequest(request) + const { journal, mainProcess } = createMainProcess() + const callback = vi.fn() + requestProxyFactory.call( + undefined, + harness.actualRequest, + false, + mainProcess + )({ hostname: 'example.test', path: '/resource' }, callback) + const response = createResponse({ + statusCode: 202, + statusMessage: 'Accepted', + headers: { 'content-type': 'text/plain', 'x-response': 'ready' } }) - describe('请求体处理', () => { - test('write 方法代理 - JSON 数据', () => { - const mockRequest = createMockClientRequest() - const originalWrite = mockRequest.write - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST' - } - const request = proxyFn(options) - - const jsonData = JSON.stringify({ key: 'value' }) - request.write(jsonData) - - // 验证原始 write 被调用 - expect(originalWrite).toHaveBeenCalledWith(jsonData) - }) - - test('write 方法代理 - 非 JSON 数据', () => { - const mockRequest = createMockClientRequest() - const originalWrite = mockRequest.write - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST' - } - const request = proxyFn(options) - - const rawData = 'plain text data' - request.write(rawData) - - // 验证原始 write 被调用 - expect(originalWrite).toHaveBeenCalledWith(rawData) - }) - - test('write 方法代理 - Buffer 数据', () => { - const mockRequest = createMockClientRequest() - const originalWrite = mockRequest.write - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'POST' - } - const request = proxyFn(options) - - const bufferData = Buffer.from('buffer data') - request.write(bufferData) - - // 验证原始 write 被调用 - expect(originalWrite).toHaveBeenCalledWith(bufferData) - }) + harness.respond(response) + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'responseRequest' + ]) + expect(journal.at(-1)!.data.request).toMatchObject({ + responseStatusCode: 202, + responseStatusText: 'Accepted', + responseHeaders: { 'content-type': 'text/plain', 'x-response': 'ready' } }) + expect(journal.at(-1)!.data.response).toBe(response) + expect(callback).toHaveBeenCalledWith(response) + }) - describe('普通 HTTP 请求处理', () => { - test('非 WebSocket 请求发送 registerRequest', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 验证 registerRequest 被调用 - expect(mockSendRequest).toHaveBeenCalledWith('registerRequest', expect.any(RequestDetail)) - }) - - test('响应回调正确处理', () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ - statusCode: 200, - headers: { 'content-type': 'application/json' } - }) - - let capturedCallback: ((res: IncomingMessage) => void) | undefined - const mockActualRequestHandler = vi - .fn() - .mockImplementation( - (_options: RequestOptions, callback: (res: IncomingMessage) => void) => { - capturedCallback = callback - return mockRequest - } - ) - const { mockMainProcess, mockResponseRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const userCallback = vi.fn() - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options, userCallback) - - // 模拟响应 - expect(capturedCallback).toBeDefined() - capturedCallback!(mockResponse as IncomingMessage) - - // 验证用户回调被调用 - expect(userCallback).toHaveBeenCalledWith(mockResponse) - // 验证 responseRequest 被调用 - expect(mockResponseRequest).toHaveBeenCalled() - }) - - test('没有用户回调时也能正常处理响应', () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ statusCode: 200 }) - - let capturedCallback: ((res: IncomingMessage) => void) | undefined - const mockActualRequestHandler = vi - .fn() - .mockImplementation( - (_options: RequestOptions, callback: (res: IncomingMessage) => void) => { - capturedCallback = callback - return mockRequest - } - ) - const { mockMainProcess, mockResponseRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - // 不传递回调 - proxyFn(options) - - // 模拟响应 - expect(capturedCallback).toBeDefined() - capturedCallback!(mockResponse as IncomingMessage) - - // 验证 responseRequest 被调用 - expect(mockResponseRequest).toHaveBeenCalled() - }) + test('reports a pre-response error only as requestFailed and only once', () => { + const request = createClientRequest({ path: '/failure' }) + const { actualRequest } = createActualRequest(request) + const { journal, mainProcess } = createMainProcess() + requestProxyFactory.call( + undefined, + actualRequest, + false, + mainProcess + )({ + hostname: 'example.test', + path: '/failure' }) - describe('错误处理', () => { - test('请求错误时发送 endRequest', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 模拟错误事件 - mockRequest.emit('error', new Error('Connection refused')) - - // 验证 endRequest 被调用,且状态码为 0 - expect(mockSendRequest).toHaveBeenCalledWith( - 'endRequest', - expect.objectContaining({ - responseStatusCode: 0 - }) - ) - }) - - test('请求错误时记录结束时间', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const beforeTime = Date.now() - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 模拟错误事件 - mockRequest.emit('error', new Error('Connection refused')) - const afterTime = Date.now() - - // 获取 endRequest 调用的参数 - const endRequestCall = mockSendRequest.mock.calls.find((call) => call[0] === 'endRequest') - expect(endRequestCall).toBeDefined() - const requestDetail = endRequestCall![1] as RequestDetail - expect(requestDetail.requestEndTime).toBeGreaterThanOrEqual(beforeTime) - expect(requestDetail.requestEndTime).toBeLessThanOrEqual(afterTime) - }) + request.emit('error', new Error('socket refused')) + request.emit('abort') + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'requestFailed' + ]) + const failure = journal.at(-1)!.data + expect(failure).toMatchObject({ errorText: 'socket refused', canceled: false }) + expect(failure.request.requestEndTime).toBeLessThan(10_000_000_000) + }) + + test('treats an early destroyed close as canceled but ignores close after response headers', () => { + const early = createClientRequest({ path: '/early-close' }) + const { actualRequest: earlyActual } = createActualRequest(early) + const earlyMain = createMainProcess() + requestProxyFactory.call( + undefined, + earlyActual, + false, + earlyMain.mainProcess + )({ + hostname: 'example.test', + path: '/early-close' }) + early.destroyed = true + early.emit('close') + expect(earlyMain.journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'registerRequest', + 'requestFailed' + ]) + expect(earlyMain.journal.at(-1)!.data.canceled).toBe(true) + + const completed = createClientRequest({ path: '/has-headers' }) + const completedHarness = createActualRequest(completed) + const completedMain = createMainProcess() + requestProxyFactory.call( + undefined, + completedHarness.actualRequest, + false, + completedMain.mainProcess + )({ hostname: 'example.test', path: '/has-headers' }) + completedHarness.respond(createResponse()) + completed.destroyed = true + completed.emit('close') + expect(completedMain.journal.some(({ type }) => type === 'requestFailed')).toBe(false) + }) + + test('get delegates to request and ends it, triggering lazy registration', () => { + const request = createClientRequest({ path: '/get' }) + const proxiedRequest = vi.fn(() => request) as unknown as RequestFn + + const returned = getProxyFactory(proxiedRequest)('http://example.test/get') + + expect(returned).toBe(request) + expect(proxiedRequest).toHaveBeenCalledWith('http://example.test/get') + expect(request.end).toHaveBeenCalledOnce() + }) +}) - describe('WebSocket 请求处理', () => { - test('WebSocket 请求 URL 转换为 ws:// 协议', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/', - method: 'GET', - headers: { - Upgrade: 'websocket', - Connection: 'Upgrade' - } - } - proxyFn(options) - - // 验证 URL 被转换为 ws:// - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: expect.stringMatching(/^ws:\/\//) - }) - ) - }) - - test('HTTPS WebSocket 请求 URL 转换为 wss:// 协议', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - true, // isHttps = true - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/', - method: 'GET', - headers: { - Upgrade: 'websocket', - Connection: 'Upgrade' - } - } - proxyFn(options) - - // 验证 URL 被转换为 wss:// - expect(mockSendRequest).toHaveBeenCalledWith( - 'initRequest', - expect.objectContaining({ - url: expect.stringMatching(/^wss:\/\//) - }) - ) - }) - - test('WebSocket 请求不发送 registerRequest', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/', - method: 'GET', - headers: { - Upgrade: 'websocket' - } - } - proxyFn(options) - - // 验证 registerRequest 没有被调用 - const registerRequestCalls = mockSendRequest.mock.calls.filter( - (call) => call[0] === 'registerRequest' - ) - expect(registerRequestCalls.length).toBe(0) - }) - - test('WebSocket upgrade 事件处理 - 发送 webSocketCreated', async () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ statusCode: 101 }) - const mockSocket = createMockSocket() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSend } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/ws', - method: 'GET', - headers: { - Upgrade: 'websocket' - } - } - proxyFn(options) - - // 模拟 upgrade 事件 - mockRequest.emit('upgrade', mockResponse, mockSocket, Buffer.alloc(0)) - - // 等待异步操作 - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 验证 webSocketCreated 消息被发送 - expect(mockSend).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'Network.webSocketCreated' - }) - ) - }) - - test('WebSocket 隐藏请求不发送消息', async () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ statusCode: 101 }) - const mockSocket = createMockSocket() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSend } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - // 使用 127.0.0.1 的隐藏 URL - const options: RequestOptions = { - hostname: '127.0.0.1', - path: '/', - method: 'GET', - headers: { - Upgrade: 'websocket' - } - } - proxyFn(options) - - // 模拟 upgrade 事件 - mockRequest.emit('upgrade', mockResponse, mockSocket, Buffer.alloc(0)) - - // 等待异步操作 - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 验证 webSocketCreated 消息没有被发送 - const webSocketCreatedCalls = mockSend.mock.calls.filter( - (call) => call[0]?.type === 'Network.webSocketCreated' - ) - expect(webSocketCreatedCalls.length).toBe(0) - }) +describe('WebSocket request capture', () => { + beforeEach(() => vi.clearAllMocks()) + + function createWebSocketHarness() { + const request = createClientRequest({ + protocol: 'http:', + host: '127.0.0.1:43917', + path: '/socket?token=dynamic', + headers: { + Upgrade: 'websocket', + Connection: 'Upgrade', + 'Sec-WebSocket-Key': 'test-key' + } }) + const actual = createActualRequest(request) + const capture = createMainProcess() + const returned = requestProxyFactory.call( + undefined, + actual.actualRequest, + false, + capture.mainProcess + )({ hostname: 'stale.invalid', path: '/wrong', headers: { Upgrade: 'websocket' } }) + return { request, returned, ...actual, ...capture } + } + + test('uses the dynamic ws URL and never registers a normal HTTP request', () => { + const { returned, journal } = createWebSocketHarness() + + returned.end() + + expect(journal.map(({ type }) => type)).toEqual(['initRequest']) + expect(journal[0].data.url).toBe('ws://127.0.0.1:43917/socket?token=dynamic') + }) - describe('WebSocket 帧处理', () => { - test('WebSocket socket close 事件发送 webSocketClosed', async () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ statusCode: 101 }) - const mockSocket = createMockSocket() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSend } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/ws', - method: 'GET', - headers: { - Upgrade: 'websocket' - } - } - proxyFn(options) - - // 模拟 upgrade 事件 - mockRequest.emit('upgrade', mockResponse, mockSocket, Buffer.alloc(0)) - - // 等待异步操作 - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 模拟 socket close 事件 - mockSocket.emit('close') - - // 等待异步操作 - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 验证 webSocketClosed 消息被发送 - expect(mockSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.webSocketClosed' - }) - ) - }) - - test('WebSocket socket end 事件正确处理', async () => { - const mockRequest = createMockClientRequest() - const mockResponse = createMockIncomingMessage({ statusCode: 101 }) - const mockSocket = createMockSocket() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/ws', - method: 'GET', - headers: { - Upgrade: 'websocket' - } - } - proxyFn(options) - - // 模拟 upgrade 事件 - mockRequest.emit('upgrade', mockResponse, mockSocket, Buffer.alloc(0)) - - // 等待异步操作 - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 模拟 socket end 事件 - 不应该抛出错误 - expect(() => mockSocket.emit('end')).not.toThrow() - }) + test('emits a serializable handshake DTO for loopback application traffic', () => { + const { request, journal } = createWebSocketHarness() + const socket = createSocket() + const response = createResponse({ + statusCode: 101, + statusMessage: 'Switching Protocols', + headers: { upgrade: 'websocket', connection: 'Upgrade' }, + rawHeaders: ['Upgrade', 'websocket', 'Connection', 'Upgrade'] }) - describe('原始请求处理器调用', () => { - test('字符串 URL 参数正确传递给原始处理器', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const callback = vi.fn() - proxyFn('http://example.com/api', { method: 'POST' }, callback) - - // 验证原始处理器被调用,参数正确 - expect(mockActualRequestHandler).toHaveBeenCalledWith( - 'http://example.com/api', - { method: 'POST' }, - expect.any(Function) - ) - }) - - test('RequestOptions 参数正确传递给原始处理器', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 验证原始处理器被调用,参数正确 - expect(mockActualRequestHandler).toHaveBeenCalledWith(options, expect.any(Function)) - }) - - test('返回代理后的 ClientRequest', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - const result = proxyFn(options) - - // 验证返回的是代理后的请求对象 - expect(result).toBeDefined() - expect(typeof result.write).toBe('function') - expect(typeof result.setHeader).toBe('function') - }) + request.emit('upgrade', response, socket, Buffer.alloc(0)) + request.destroyed = true + request.emit('close') + + expect(journal.map(({ type }) => type)).toEqual(['initRequest', 'Network.webSocketCreated']) + const created = journal.at(-1)!.data + expect(created).toMatchObject({ + requestId: journal[0].data.id, + url: 'ws://127.0.0.1:43917/socket?token=dynamic', + response: { + httpVersion: '1.1', + statusCode: 101, + statusMessage: 'Switching Protocols', + rawHeaders: ['Upgrade', 'websocket', 'Connection', 'Upgrade'], + headers: { upgrade: 'websocket', connection: 'Upgrade' } + } }) + expect(Object.keys(created).sort()).toEqual(['initiator', 'requestId', 'response', 'url']) + expect(() => JSON.stringify(created)).not.toThrow() + }) - describe('调用栈加载', () => { - test('请求创建时加载调用栈', () => { - const mockRequest = createMockClientRequest() - const mockActualRequestHandler = vi.fn().mockReturnValue(mockRequest) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 获取 initRequest 调用的参数 - const initRequestCall = mockSendRequest.mock.calls.find((call) => call[0] === 'initRequest') - expect(initRequestCall).toBeDefined() - const requestDetail = initRequestCall![1] as RequestDetail - - // 验证 RequestDetail 实例被创建 - expect(requestDetail).toBeInstanceOf(RequestDetail) - }) + test('emits text, binary, and one close DTO with the correct frame encoding', async () => { + const { request, journal } = createWebSocketHarness() + const socket = createSocket() + request.emit('upgrade', createResponse({ statusCode: 101 }), socket, Buffer.alloc(0)) + + socket.emit('data', websocketFrame(Buffer.from('server text'), 1, false)) + socket.write(websocketFrame(Buffer.from([0, 255, 16]), 2, true)) + socket.emit('end') + socket.emit('close') + + await vi.waitFor(() => expect(journal).toHaveLength(5)) + + expect(journal.map(({ type }) => type)).toEqual([ + 'initRequest', + 'Network.webSocketCreated', + 'Network.webSocketFrameReceived', + 'Network.webSocketFrameSent', + 'Network.webSocketClosed' + ]) + expect(journal[2].data.response).toEqual({ + payloadData: 'server text', + opcode: 1, + mask: false + }) + expect(journal[3].data.response).toEqual({ + payloadData: Buffer.from([0, 255, 16]).toString('base64'), + opcode: 2, + mask: true }) + expect(journal.filter(({ type }) => type === 'Network.webSocketClosed')).toHaveLength(1) + }) - describe('响应头处理', () => { - test('响应头正确记录到 RequestDetail', () => { - const mockRequest = createMockClientRequest() - const responseHeaders = { - 'content-type': 'application/json', - 'x-custom-header': 'custom-value' - } - const mockResponse = createMockIncomingMessage({ - statusCode: 200, - headers: responseHeaders - }) - - let capturedCallback: ((res: IncomingMessage) => void) | undefined - const mockActualRequestHandler = vi - .fn() - .mockImplementation( - (_options: RequestOptions, callback: (res: IncomingMessage) => void) => { - capturedCallback = callback - return mockRequest - } - ) - const { mockMainProcess, mockSendRequest } = createMockMainProcess() - - const proxyFn = requestProxyFactory.call( - null, - mockActualRequestHandler, - false, - mockMainProcess as never - ) - - const options: RequestOptions = { - hostname: 'example.com', - path: '/api', - method: 'GET' - } - proxyFn(options) - - // 模拟响应 - expect(capturedCallback).toBeDefined() - capturedCallback!(mockResponse as IncomingMessage) - - // 获取 initRequest 调用的参数,验证 responseHeaders 被设置 - const initRequestCall = mockSendRequest.mock.calls.find((call) => call[0] === 'initRequest') - expect(initRequestCall).toBeDefined() - const requestDetail = initRequestCall![1] as RequestDetail - expect(requestDetail.responseHeaders).toEqual(responseHeaders) - }) + test('decodes negotiated permessage-deflate frames in both directions', async () => { + const { request, journal } = createWebSocketHarness() + const socket = createSocket() + const negotiated = { + server_no_context_takeover: [true], + client_no_context_takeover: [true] + } + const response = createResponse({ + statusCode: 101, + headers: { + upgrade: 'websocket', + connection: 'Upgrade', + 'sec-websocket-extensions': + 'permessage-deflate; server_no_context_takeover; client_no_context_takeover' + } + }) + request.emit('upgrade', response, socket, Buffer.alloc(0)) + + const serverExtension = new PerMessageDeflate( + { serverNoContextTakeover: true, clientNoContextTakeover: true }, + true + ) + serverExtension.accept([structuredClone(negotiated)]) + const clientExtension = new PerMessageDeflate( + { serverNoContextTakeover: true, clientNoContextTakeover: true }, + false + ) + clientExtension.accept([structuredClone(negotiated)]) + + const receivedPayload = Buffer.from('compressed server text') + const sentPayload = Buffer.from([0, 255, 16, 32, 64]) + socket.emit( + 'data', + websocketFrame(await compress(serverExtension, receivedPayload), 1, false, true) + ) + const sentFrame = websocketFrame(await compress(clientExtension, sentPayload), 2, true, true) + const applicationFrame = Buffer.from(sentFrame) + socket.write(sentFrame) + expect(sentFrame).toEqual(applicationFrame) + + await vi.waitFor(() => { + expect(journal.filter(({ type }) => type.includes('webSocketFrame'))).toHaveLength(2) + }) + expect(journal.at(-2)?.data.response).toEqual({ + payloadData: receivedPayload.toString(), + opcode: 1, + mask: false }) + expect(journal.at(-1)?.data.response).toEqual({ + payloadData: sentPayload.toString('base64'), + opcode: 2, + mask: true + }) + + socket.emit('close') + serverExtension.cleanup() + clientExtension.cleanup() + }) + + test('contains asynchronous Receiver errors instead of crashing application I/O', async () => { + const { request, journal } = createWebSocketHarness() + const socket = createSocket() + request.emit('upgrade', createResponse({ statusCode: 101 }), socket, Buffer.alloc(0)) + + // RSV1 without a negotiated extension is invalid and fails through the + // Writable callback, not through the synchronous write() call. + socket.emit('data', websocketFrame(Buffer.from('invalid'), 1, false, true)) + await new Promise((resolve) => setImmediate(resolve)) + + expect(journal.map(({ type }) => type)).toEqual(['initRequest', 'Network.webSocketCreated']) + expect(() => socket.emit('close')).not.toThrow() }) }) diff --git a/packages/network-debugger/src/core/request.ts b/packages/network-debugger/src/core/request.ts index a07b611..89c1a2b 100644 --- a/packages/network-debugger/src/core/request.ts +++ b/packages/network-debugger/src/core/request.ts @@ -1,10 +1,13 @@ -import { ClientRequest, IncomingMessage, RequestOptions } from 'http' -import { Socket } from 'node:net' +import type { ClientRequest, IncomingMessage, RequestOptions } from 'node:http' +import type { Socket } from 'node:net' import { RequestDetail } from '../common' -import { getTimestamp } from '../utils' -import { MainProcess } from './fork' +import type { LegacyWebSocketHandshake } from '../legacy-bridge/contracts' +import type { MainProcess } from './fork' import { BINARY_TYPES } from './ws/constants' +import { parsePerMessageDeflate } from './ws/extension' +import PerMessageDeflate from './ws/permessage-deflate' import { Receiver } from './ws/receiver' +import { isLegacyCaptureSuppressed } from './capture-scope' export interface RequestFn { (options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest @@ -15,247 +18,413 @@ export interface RequestFn { ): ClientRequest } -function proxyClientRequestFactory( - actualRequest: ClientRequest, - requestDetail: RequestDetail, - mainProcess: MainProcess -) { - const actualFn = actualRequest.write - actualRequest.write = (data: any) => { - // Convert to string at source to avoid IPC serialization issues - // Accumulate multiple writes (e.g., multipart/form-data) - let chunk: string - if (Buffer.isBuffer(data)) { - chunk = data.toString('utf-8') - } else if (typeof data === 'string') { - chunk = data - } else if (data != null) { - chunk = JSON.stringify(data) - } else { - chunk = '' - } - - if (requestDetail.requestData) { - requestDetail.requestData += chunk - } else { - requestDetail.requestData = chunk - } +function errorText(error: unknown): string { + if (error instanceof Error) return error.message + return String(error || 'Request failed') +} - return actualFn.bind(actualRequest)(data) +function responseHandshake(response: IncomingMessage): LegacyWebSocketHandshake { + return { + httpVersion: response.httpVersion, + statusCode: response.statusCode ?? 101, + statusMessage: response.statusMessage ?? 'Switching Protocols', + rawHeaders: [...response.rawHeaders], + headers: { ...response.headers } } +} - actualRequest.on('error', () => { - requestDetail.responseStatusCode = 0 - requestDetail.requestEndTime = new Date().getTime() - mainProcess.sendRequest('endRequest', requestDetail) - }) +function framePayload(data: unknown, isBinary: boolean): string { + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data as any) + return isBinary ? buffer.toString('base64') : buffer.toString('utf8') +} - if (requestDetail.isWebSocket()) { - actualRequest.on('upgrade', async (res: IncomingMessage, socket: Socket, head: Buffer) => { - const originalWrite = socket.write +function cloneExtensionConfigurations( + configurations: readonly Record[] +): Record[] { + return configurations.map((parameters) => + Object.fromEntries(Object.entries(parameters).map(([name, values]) => [name, [...values]])) + ) +} - if (requestDetail.isHiden()) { - return - } +function negotiatedWebSocketExtensions(response: IncomingMessage): { + receiver: Record + sender: Record + cleanupReceiver(): void + cleanupSender(): void +} { + let receiverExtension: PerMessageDeflate | undefined + let senderExtension: PerMessageDeflate | undefined - await mainProcess.send({ - type: 'Network.webSocketCreated', - data: { - requestId: requestDetail.id, - url: requestDetail.url, - initiator: requestDetail.initiator, - response: res - } - }) + try { + const configurations = parsePerMessageDeflate(response.headers['sec-websocket-extensions']) + if (configurations) { + // Incoming bytes are server -> client, while intercepted socket writes + // are client -> server. Each direction needs its own zlib context. + receiverExtension = new PerMessageDeflate({}, false) + receiverExtension.accept(cloneExtensionConfigurations(configurations)) + senderExtension = new PerMessageDeflate({}, true) + senderExtension.accept(cloneExtensionConfigurations(configurations)) + } + } catch { + // The owning WebSocket implementation validates the handshake. Capture is + // observational and must never crash the application for a bad extension. + receiverExtension?.cleanup() + senderExtension?.cleanup() + receiverExtension = undefined + senderExtension = undefined + } - const receiver = new Receiver({ - allowSynchronousEvents: true, - binaryType: BINARY_TYPES[0], - isServer: false - }) - const sender = new Receiver({ - allowSynchronousEvents: true, - binaryType: BINARY_TYPES[0], - isServer: true - }) + return { + receiver: receiverExtension ? { [PerMessageDeflate.extensionName]: receiverExtension } : {}, + sender: senderExtension ? { [PerMessageDeflate.extensionName]: senderExtension } : {}, + cleanupReceiver: () => receiverExtension?.cleanup(), + cleanupSender: () => senderExtension?.cleanup() + } +} - const receiverHandler = (data: any) => { - const str = data.toString() - // const socketMessage = jsonParse(str, str) - mainProcess.send({ - type: 'Network.webSocketFrameReceived', - data: { - requestId: requestDetail.id, - response: { - payloadData: str, - opcode: 1, - mask: false - } - } - }) - } +function writeCapture(receiver: Receiver, data: unknown): void { + if (receiver.writableEnded || receiver.destroyed) return + try { + // Receiver unmasks client frames in place. Always copy so observation can + // never mutate the bytes that the application is about to put on the wire. + receiver.write(Buffer.from(data as any)) + } catch { + // A malformed frame or socket shutdown must not affect application I/O. + } +} - const senderHanlder = (data: any) => { - const str = data.toString() - mainProcess.send({ - type: 'Network.webSocketFrameSent', - data: { - requestId: requestDetail.id, - response: { - payloadData: str, - opcode: 1, - mask: true - } - } - }) +function endCapture(receiver: Receiver): void { + if (receiver.writableEnded || receiver.destroyed) return + try { + receiver.end() + } catch { + // Capture teardown remains isolated from the application socket. + } +} + +function installWebSocketCapture( + request: ClientRequest, + requestDetail: RequestDetail, + mainProcess: MainProcess +): void { + request.on('upgrade', (response: IncomingMessage, socket: Socket, head: Buffer) => { + void mainProcess.send({ + type: 'Network.webSocketCreated', + data: { + requestId: requestDetail.id, + url: requestDetail.url ?? '', + initiator: requestDetail.initiator, + response: responseHandshake(response) } + }) - receiver.on('message', receiverHandler) - sender.on('message', senderHanlder) - let chunk + const extensions = negotiatedWebSocketExtensions(response) + const receiver = new Receiver({ + allowSynchronousEvents: true, + binaryType: BINARY_TYPES[0], + extensions: extensions.receiver, + isServer: false + }) + const sender = new Receiver({ + allowSynchronousEvents: true, + binaryType: BINARY_TYPES[0], + extensions: extensions.sender, + isServer: true + }) - socket.write = (data: any, ...rest: any[]) => { - const buf = Buffer.from(data) - sender.write(buf) - return originalWrite.call(socket, data, ...rest) - } - socket.addListener('data', (data) => { - const buf = Buffer.from(data) - receiver.write(buf) + // Writable reports parser failures asynchronously. Retaining these + // listeners is what keeps observational capture errors out of user code. + receiver.on('error', () => undefined) + sender.on('error', () => undefined) + let closeRequested = false + let closeSent = false + let receiverSettled = false + let senderSettled = false + const sendClosedWhenSettled = () => { + if (!closeRequested || closeSent || !receiverSettled || !senderSettled) return + closeSent = true + void mainProcess.send({ + type: 'Network.webSocketClosed', + data: { requestId: requestDetail.id } }) - socket.addListener('close', () => { - chunk = socket.read() - if (chunk !== null) { - receiver.write(chunk) - sender.write(chunk) - } - receiver.end() - sender.end() - receiver.removeAllListeners() - sender.removeAllListeners() - mainProcess.send({ - method: 'Network.webSocketClosed', - params: { - requestId: requestDetail.id, - timestamp: getTimestamp() + } + const settleReceiver = () => { + if (receiverSettled) return + receiverSettled = true + extensions.cleanupReceiver() + sendClosedWhenSettled() + } + const settleSender = () => { + if (senderSettled) return + senderSettled = true + extensions.cleanupSender() + sendClosedWhenSettled() + } + receiver.once('finish', settleReceiver) + receiver.once('close', settleReceiver) + sender.once('finish', settleSender) + sender.once('close', settleSender) + + receiver.on('message', (data: unknown, isBinary: boolean) => { + void mainProcess.send({ + type: 'Network.webSocketFrameReceived', + data: { + requestId: requestDetail.id, + response: { + payloadData: framePayload(data, isBinary), + opcode: isBinary ? 2 : 1, + mask: false } - }) + } }) - socket.addListener('end', () => { - receiver.end() - sender.end() - receiver.removeAllListeners() - sender.removeAllListeners() + }) + sender.on('message', (data: unknown, isBinary: boolean) => { + void mainProcess.send({ + type: 'Network.webSocketFrameSent', + data: { + requestId: requestDetail.id, + response: { + payloadData: framePayload(data, isBinary), + opcode: isBinary ? 2 : 1, + mask: true + } + } }) }) - } else { - mainProcess.sendRequest('registerRequest', requestDetail) - } - return actualRequest -} + const originalWrite = socket.write + socket.write = function (this: Socket, data: any, ...rest: any[]) { + writeCapture(sender, data) + return Reflect.apply(originalWrite, this, [data, ...rest]) + } as typeof socket.write -function proxyCallbackFactory( - actualCallBack: any, - requestDetail: RequestDetail, - mainProcess: MainProcess -) { - return (response: IncomingMessage) => { - requestDetail.responseHeaders = response.headers - if (typeof actualCallBack === 'function') { - actualCallBack(response) + if (head.length > 0) writeCapture(receiver, head) + socket.on('data', (data) => writeCapture(receiver, data)) + + const close = () => { + if (closeRequested) return + closeRequested = true + endCapture(receiver) + endCapture(sender) + sendClosedWhenSettled() } + socket.on('close', close) + socket.on('end', close) + }) +} + +function initialUrl( + arg1: RequestOptions | string | URL, + options: RequestOptions | undefined, + isHttps: boolean +): string { + if (typeof arg1 === 'string') return arg1 + if (arg1 instanceof URL) return arg1.toString() + const protocol = options?.protocol ?? (isHttps ? 'https:' : 'http:') + const hostname = options?.hostname ?? options?.host ?? 'localhost' + const port = options?.port === undefined ? '' : `:${options.port}` + return `${protocol}//${hostname}${port}${options?.path ?? '/'}` +} - mainProcess.responseRequest(requestDetail.id, response) +function requestOptions( + arg1: RequestOptions | string | URL, + arg2: RequestOptions | ((response: IncomingMessage) => void) | undefined +): RequestOptions | undefined { + if (typeof arg1 === 'string' || arg1 instanceof URL) { + return typeof arg2 === 'object' ? arg2 : undefined } + return arg1 } -function proxySetHeader(request: ClientRequest, requestDetail: RequestDetail) { - let originSetHeader = request.setHeader - request.setHeader = function (name, val) { - if (Array.isArray(val)) { - // TODO: use string[] to send multiple headers with the same name. - val.forEach((v) => { - requestDetail.requestHeaders[name] = v - }) - } else { - requestDetail.requestHeaders[name] = val - } - return originSetHeader.call(request, name, val) +function callbackFrom( + arg2: RequestOptions | ((response: IncomingMessage) => void) | undefined, + arg3: ((response: IncomingMessage) => void) | undefined +): ((response: IncomingMessage) => void) | undefined { + return typeof arg2 === 'function' ? arg2 : arg3 +} + +function invokeRequest( + actualRequestHandler: RequestFn, + thisValue: unknown, + arg1: RequestOptions | string | URL, + arg2: RequestOptions | ((response: IncomingMessage) => void) | undefined, + callback: (response: IncomingMessage) => void +): ClientRequest { + let args: unknown[] + if (typeof arg1 === 'string' || arg1 instanceof URL) { + args = typeof arg2 === 'object' ? [arg1, arg2, callback] : [arg1, callback] + } else { + args = [arg1, callback] } + return Reflect.apply(actualRequestHandler, thisValue, args) } +/** + * Wrap `http.request`/`https.request` without changing their overload or stream + * semantics. Capture is registered immediately before `end()` (or before an + * early response/failure) so every `write()` chunk and `end(body)` mutation is + * visible in the CDP request. + */ export function requestProxyFactory( - this: any, - actualRequestHandler: any, + this: unknown, + actualRequestHandler: RequestFn, isHttps: boolean, mainProcess: MainProcess -) { - const fn: RequestFn = (arg1: any, arg2?: any, arg3?: any) => { - // #region resolve arguments - let url: string | URL | undefined - let options: RequestOptions | string | URL | undefined - let callback: ((res: IncomingMessage) => void) | undefined - - if (typeof arg1 === 'string' || arg1 instanceof URL) { - // Signature: (url: string | URL, options: RequestOptions, callback?: (res: IncomingMessage) => void): ClientRequest; - url = arg1 - options = arg2 - callback = arg3 - } else { - // Signature: (options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest; - options = arg1 - callback = arg2 +): RequestFn { + const thisValue = this + return function requestProxy( + arg1: RequestOptions | string | URL, + arg2?: RequestOptions | ((response: IncomingMessage) => void), + arg3?: (response: IncomingMessage) => void + ): ClientRequest { + if (isLegacyCaptureSuppressed()) { + const args = + typeof arg1 === 'string' || arg1 instanceof URL + ? typeof arg2 === 'object' + ? [arg1, arg2, arg3] + : [arg1, arg2] + : [arg1, arg2] + return Reflect.apply(actualRequestHandler, thisValue, args) } + const options = requestOptions(arg1, arg2) + const actualCallback = callbackFrom(arg2, arg3) + const detail = new RequestDetail() + detail.requestStartTime = Date.now() / 1000 + detail.url = initialUrl(arg1, options, isHttps) + detail.method = options?.method ?? 'GET' + detail.requestHeaders = { ...(options?.headers ?? {}) } + detail.responseHeaders = {} + detail.loadCallFrames() - const requestDetail = new RequestDetail() - - if (typeof url === 'string') { - requestDetail.url = url - requestDetail.method = 'GET' - } else if (url instanceof URL) { - requestDetail.url = url.toString() - requestDetail.method = 'GET' - } else if (options && typeof options !== 'string' && !(options instanceof URL)) { - const connectionType = isHttps ? 'https' : 'http' - requestDetail.url = `${connectionType}://${options.hostname || options.host}${options.path}` + let responseStarted = false + let terminal = false + let request!: ClientRequest + + const fail = (error: unknown, canceled = false) => { + if (terminal) return + terminal = true + register() + detail.requestEndTime = Date.now() / 1000 + void mainProcess.send({ + type: 'requestFailed', + data: { request: detail, errorText: errorText(error), canceled } + }) } - if (options && typeof options !== 'string' && !(options instanceof URL)) { - requestDetail.method = options.method - requestDetail.requestHeaders = options.headers + const proxyCallback = (response: IncomingMessage) => { + responseStarted = true + register() + detail.responseHeaders = { ...response.headers } + detail.responseStatusCode = response.statusCode ?? 0 + detail.responseStatusText = response.statusMessage ?? '' + mainProcess.responseRequest(detail, response) + actualCallback?.(response) } - // #endregion + request = invokeRequest(actualRequestHandler, thisValue, arg1, arg2, proxyCallback) - requestDetail.loadCallFrames() - if (requestDetail.isWebSocket()) { - requestDetail.url = requestDetail - .url!.replace('http://', 'ws://') - .replace('https://', 'wss://') - mainProcess.sendRequest('initRequest', requestDetail) - } else { - mainProcess.sendRequest('initRequest', requestDetail) + const protocol = (request as ClientRequest & { protocol?: string }).protocol + const hostHeader = request.getHeader('host') + const host = + typeof hostHeader === 'string' || typeof hostHeader === 'number' + ? String(hostHeader) + : (request as ClientRequest & { host?: string }).host + const path = request.path + if (protocol && host && path) detail.url = `${protocol}//${host}${path}` + detail.method = request.method || detail.method || 'GET' + if (typeof request.getHeaders === 'function') detail.requestHeaders = request.getHeaders() + if (detail.isWebSocket()) { + detail.url = detail.url?.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:') } - const proxyCallback = proxyCallbackFactory(callback, requestDetail, mainProcess) + mainProcess.sendRequest('initRequest', detail) - if (typeof arg1 === 'string' || arg1 instanceof URL) { - // Call actualRequestHandler with 3 parameters - const request: ClientRequest = actualRequestHandler( - url!, - options as RequestOptions, - proxyCallback + const bodyChunks: Buffer[] = [] + let registered = false + const appendBody = (chunk: unknown, encoding?: BufferEncoding) => { + if (chunk === undefined || chunk === null) return + const value = Buffer.isBuffer(chunk) + ? chunk + : chunk instanceof Uint8Array + ? Buffer.from(chunk) + : Buffer.from(String(chunk), encoding) + bodyChunks.push(value) + } + const register = () => { + if (registered || detail.isWebSocket()) return + registered = true + if (typeof request.getHeaders === 'function') detail.requestHeaders = request.getHeaders() + if (bodyChunks.length > 0) detail.requestData = Buffer.concat(bodyChunks) + mainProcess.sendRequest('registerRequest', detail) + } + + const originalWrite = request.write + request.write = function ( + this: ClientRequest, + chunk: any, + encodingOrCallback?: any, + callback?: any + ) { + appendBody( + chunk, + typeof encodingOrCallback === 'string' ? (encodingOrCallback as BufferEncoding) : undefined ) - proxySetHeader(request, requestDetail) - return proxyClientRequestFactory(request, requestDetail, mainProcess) - } else { - // Call actualRequestHandler with 2 parameters - const request: ClientRequest = actualRequestHandler(options as RequestOptions, proxyCallback) - proxySetHeader(request, requestDetail) - return proxyClientRequestFactory(request, requestDetail, mainProcess) + return Reflect.apply(originalWrite, this, arguments) + } as typeof request.write + + const originalEnd = request.end + request.end = function ( + this: ClientRequest, + chunk?: any, + encodingOrCallback?: any, + callback?: any + ) { + if (typeof chunk !== 'function') { + appendBody( + chunk, + typeof encodingOrCallback === 'string' + ? (encodingOrCallback as BufferEncoding) + : undefined + ) + } + register() + return Reflect.apply(originalEnd, this, arguments) + } as typeof request.end + + request.on('error', (error) => fail(error)) + request.on('abort', () => fail(new Error('Request aborted'), true)) + request.on('response', () => { + responseStarted = true + }) + + if (detail.isWebSocket()) { + request.on('upgrade', () => { + // A successful 101 is terminal for ClientRequest; the upgraded socket + // owns the rest of the lifecycle. Its later request `close` is not an + // HTTP loading failure. + responseStarted = true + terminal = true + }) + installWebSocketCapture(request, detail, mainProcess) } - } - return fn + // A request can close without an error before receiving headers (for + // example, a locally destroyed request). Treat only that state as failure. + request.on('close', () => { + if (!responseStarted && !terminal && request.destroyed) { + fail(new Error('Request closed before a response was received'), true) + } + }) + + return request + } as RequestFn +} + +/** Node implements `get()` as `request()` followed by `end()`. */ +export function getProxyFactory(request: RequestFn): RequestFn { + return function getProxy(this: unknown, ...args: any[]) { + const clientRequest = Reflect.apply(request, this, args) + clientRequest.end() + return clientRequest + } as RequestFn } diff --git a/packages/network-debugger/src/core/undici.ts b/packages/network-debugger/src/core/undici.ts index 7f82e1a..8eff4c2 100644 --- a/packages/network-debugger/src/core/undici.ts +++ b/packages/network-debugger/src/core/undici.ts @@ -1,20 +1,26 @@ import { fetchProxyFactory } from './fetch' import undici from 'undici' import { MainProcess } from './fork' +import type { LegacyMockRule } from '../mock' -export const undiciFetchProxy = (mainProcess: MainProcess) => { +export const undiciFetchProxy = ( + mainProcess: MainProcess, + mockRules: readonly LegacyMockRule[] = [] +) => { if (!undici.fetch) { return } const originalFetch = undici.fetch - undici['fetch'] = fetchProxyFactory( - originalFetch as typeof globalThis.fetch, - mainProcess + const proxy = ( + mockRules.length > 0 + ? fetchProxyFactory(originalFetch as typeof globalThis.fetch, mainProcess, mockRules) + : fetchProxyFactory(originalFetch as typeof globalThis.fetch, mainProcess) ) as typeof undici.fetch + undici['fetch'] = proxy return () => { - undici['fetch'] = originalFetch + if (undici.fetch === proxy) undici['fetch'] = originalFetch } } diff --git a/packages/network-debugger/src/core/ws/extension.test.ts b/packages/network-debugger/src/core/ws/extension.test.ts new file mode 100644 index 0000000..3ed262a --- /dev/null +++ b/packages/network-debugger/src/core/ws/extension.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'vitest' +import { parsePerMessageDeflate } from './extension' + +describe('Sec-WebSocket-Extensions parsing', () => { + test('extracts negotiated permessage-deflate parameters and quoted values', () => { + expect( + parsePerMessageDeflate( + 'x-ignored; value=1, permessage-deflate; server_no_context_takeover; client_max_window_bits="12"' + ) + ).toEqual([ + { + server_no_context_takeover: [true], + client_max_window_bits: ['12'] + } + ]) + }) + + test('retains duplicate parameters so negotiation validation can reject them', () => { + expect( + parsePerMessageDeflate( + 'permessage-deflate; server_max_window_bits=12; server_max_window_bits=13' + ) + ).toEqual([{ server_max_window_bits: ['12', '13'] }]) + }) + + test('rejects malformed quoted values without evaluating them', () => { + expect(() => parsePerMessageDeflate('permessage-deflate; server_max_window_bits="12')).toThrow( + SyntaxError + ) + }) +}) diff --git a/packages/network-debugger/src/core/ws/extension.ts b/packages/network-debugger/src/core/ws/extension.ts new file mode 100644 index 0000000..67951fc --- /dev/null +++ b/packages/network-debugger/src/core/ws/extension.ts @@ -0,0 +1,98 @@ +type ExtensionParameter = string | true +type ExtensionParameters = Record + +const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ + +function splitOutsideQuotes(value: string, delimiter: ',' | ';'): string[] { + const parts: string[] = [] + let start = 0 + let quoted = false + let escaped = false + + for (let index = 0; index < value.length; index += 1) { + const character = value[index] + if (escaped) { + escaped = false + continue + } + if (quoted && character === '\\') { + escaped = true + continue + } + if (character === '"') { + quoted = !quoted + continue + } + if (!quoted && character === delimiter) { + parts.push(value.slice(start, index)) + start = index + 1 + } + } + + if (quoted || escaped) throw new SyntaxError('Invalid Sec-WebSocket-Extensions header') + parts.push(value.slice(start)) + return parts +} + +function parseToken(value: string): string { + const token = value.trim() + if (!TOKEN.test(token)) throw new SyntaxError('Invalid Sec-WebSocket-Extensions token') + return token +} + +function parseValue(value: string): string { + const candidate = value.trim() + if (!candidate.startsWith('"')) return parseToken(candidate) + if (!candidate.endsWith('"') || candidate.length < 3) { + throw new SyntaxError('Invalid Sec-WebSocket-Extensions quoted value') + } + + let result = '' + let escaped = false + for (const character of candidate.slice(1, -1)) { + if (escaped) { + result += character + escaped = false + } else if (character === '\\') { + escaped = true + } else { + result += character + } + } + if (escaped || !TOKEN.test(result)) { + throw new SyntaxError('Invalid Sec-WebSocket-Extensions quoted value') + } + return result +} + +/** + * Parse the negotiated permessage-deflate response parameters. The HTTP + * upgrade is observed below the WebSocket implementation, so capture needs an + * independent, transport-only view of the negotiated extension. + */ +export function parsePerMessageDeflate( + header: string | readonly string[] | undefined +): ExtensionParameters[] | undefined { + if (header === undefined) return undefined + const configurations: ExtensionParameters[] = [] + + for (const extension of splitOutsideQuotes( + typeof header === 'string' ? header : header.join(','), + ',' + )) { + const fields = splitOutsideQuotes(extension, ';') + const name = parseToken(fields.shift() ?? '').toLowerCase() + if (name !== 'permessage-deflate') continue + + const parameters: ExtensionParameters = Object.create(null) + for (const field of fields) { + const equals = field.indexOf('=') + const parameterName = parseToken(equals === -1 ? field : field.slice(0, equals)).toLowerCase() + const parameterValue = equals === -1 ? true : parseValue(field.slice(equals + 1)) + ;(parameters[parameterName] ??= []).push(parameterValue) + } + configurations.push(parameters) + } + + return configurations.length > 0 ? configurations : undefined +} diff --git a/packages/network-debugger/src/core/ws/permessage-deflate.test.ts b/packages/network-debugger/src/core/ws/permessage-deflate.test.ts index e84e6ac..be9578d 100644 --- a/packages/network-debugger/src/core/ws/permessage-deflate.test.ts +++ b/packages/network-debugger/src/core/ws/permessage-deflate.test.ts @@ -319,6 +319,38 @@ describe('PerMessageDeflate', () => { clientPmd.cleanup() }) + it('应该在连续消息之间清空缓冲并维护压缩上下文', async () => { + const serverPmd = new PerMessageDeflate({}, true) + serverPmd.accept([{}]) + const clientPmd = new PerMessageDeflate({}, false) + clientPmd.accept([{}]) + + const compress = (data: string) => + new Promise((resolve, reject) => { + serverPmd.compress(data, true, (error, result) => { + if (error) reject(error) + else if (!result) reject(new Error('Compression returned no data')) + else resolve(result) + }) + }) + const decompress = (data: Buffer) => + new Promise((resolve, reject) => { + clientPmd.decompress(data, true, (error, result) => { + if (error) reject(error) + else if (!result) reject(new Error('Decompression returned no data')) + else resolve(result) + }) + }) + + const first = await decompress(await compress('first compressed message')) + const second = await decompress(await compress('second compressed message')) + + expect(first.toString()).toBe('first compressed message') + expect(second.toString()).toBe('second compressed message') + serverPmd.cleanup() + clientPmd.cleanup() + }) + it('应该在超过 maxPayload 时返回错误(解压缩)', async () => { const serverPmd = new PerMessageDeflate({}, true) serverPmd.accept([{}]) diff --git a/packages/network-debugger/src/core/ws/permessage-deflate.ts b/packages/network-debugger/src/core/ws/permessage-deflate.ts index fe870ea..e67595b 100644 --- a/packages/network-debugger/src/core/ws/permessage-deflate.ts +++ b/packages/network-debugger/src/core/ws/permessage-deflate.ts @@ -272,32 +272,43 @@ export class PerMessageDeflate { this._inflate.on('data', inflateOnData) } - ;(this._inflate as any)[kCallback] = callback + const inflate = this._inflate + const state = inflate as any + state[kCallback] = callback - this._inflate.write(data) - if (fin) this._inflate.write(TRAILER) + inflate.write(data) + if (fin) inflate.write(TRAILER) - this._inflate.flush(() => { - const err = (this._inflate as any)[kError] + inflate.flush(() => { + // An inflate error calls the write callback and clears `_inflate`. + if (this._inflate !== inflate) return + const err = state[kError] if (err) { - this._inflate!.close() + inflate.close() this._inflate = null + state[kCallback] = undefined callback(err) return } - const data = bufferUtil.concat( - (this._inflate as any)[kBuffers], - (this._inflate as any)[kTotalLength] - ) + const result = bufferUtil.concat(state[kBuffers], state[kTotalLength]) + state[kCallback] = undefined - if (this._maxPayload < 1 || data.length <= this._maxPayload) { - callback(null, data) - return + if (state._readableState?.endEmitted) { + inflate.close() + this._inflate = null + } else { + state[kTotalLength] = 0 + state[kBuffers] = [] + if (fin && this.params?.[`${endpoint}_no_context_takeover`]) inflate.reset() } - callback(new RangeError('Max payload size exceeded'), null) + if (this._maxPayload > 0 && result.length > this._maxPayload) { + callback(new RangeError('Max payload size exceeded'), null) + } else { + callback(null, result) + } }) } @@ -319,48 +330,66 @@ export class PerMessageDeflate { ...this._options.zlibDeflateOptions, windowBits }) + ;(this._deflate as any)[kPerMessageDeflate] = this ;(this._deflate as any)[kTotalLength] = 0 ;(this._deflate as any)[kBuffers] = [] this._deflate.on('error', deflateOnError) this._deflate.on('data', deflateOnData) } - ;(this._deflate as any)[kCallback] = callback + const deflate = this._deflate + const state = deflate as any + state[kCallback] = callback - this._deflate.write(data) - if (fin) - this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { - const data = bufferUtil.concat( - (this._deflate as any)[kBuffers], - (this._deflate as any)[kTotalLength] - ) + deflate.write(data) + deflate.flush(zlib.constants.Z_SYNC_FLUSH, () => { + // Cleanup or a zlib error can close the stream while work is pending. + if (this._deflate !== deflate) return + let result = bufferUtil.concat(state[kBuffers], state[kTotalLength]) + if (fin) result = result.subarray(0, Math.max(0, result.length - TRAILER.length)) - if (this._maxPayload < 1 || data.length <= this._maxPayload) { - callback(null, data) - return - } + state[kCallback] = undefined + state[kTotalLength] = 0 + state[kBuffers] = [] + if (fin && this.params?.[`${endpoint}_no_context_takeover`]) deflate.reset() + if (this._maxPayload > 0 && result.length > this._maxPayload) { callback(new RangeError('Max payload size exceeded'), null) - }) + } else { + callback(null, result) + } + }) } } -function inflateOnError(this: PerMessageDeflate, err: Error): void { - this[kPerMessageDeflate]![kCallback]!(err) +function inflateOnError(this: zlib.InflateRaw, err: Error): void { + const state = this as any + const extension = state[kPerMessageDeflate] as PerMessageDeflate | undefined + if (extension) (extension as any)._inflate = null + const callback = state[kCallback] as ((error: Error) => void) | undefined + state[kCallback] = undefined + if (callback) callback(err) } -function inflateOnData(this: PerMessageDeflate, chunk: Buffer): void { - this[kTotalLength]! += chunk.length - this[kBuffers]!.push(chunk) +function inflateOnData(this: zlib.InflateRaw, chunk: Buffer): void { + const state = this as any + state[kTotalLength] += chunk.length + state[kBuffers].push(chunk) } -function deflateOnError(this: PerMessageDeflate, err: Error): void { - this[kPerMessageDeflate]![kCallback]!(err) +function deflateOnError(this: zlib.DeflateRaw, err: Error): void { + const state = this as any + const extension = state[kPerMessageDeflate] as PerMessageDeflate | undefined + if (extension) (extension as any)._deflate = null + const callback = state[kCallback] as ((error: Error) => void) | undefined + state[kCallback] = undefined + if (callback) callback(err) } -function deflateOnData(this: PerMessageDeflate, chunk: Buffer): void { - this[kTotalLength]! += chunk.length - this[kBuffers]!.push(chunk) +function deflateOnData(this: zlib.DeflateRaw, chunk: Buffer): void { + const state = this as any + state[kTotalLength] += chunk.length + state[kBuffers].push(chunk) } export default PerMessageDeflate diff --git a/packages/network-debugger/src/diagnostics/doctor.test.ts b/packages/network-debugger/src/diagnostics/doctor.test.ts new file mode 100644 index 0000000..008eeb7 --- /dev/null +++ b/packages/network-debugger/src/diagnostics/doctor.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it, vi } from 'vitest' +import type { + AdapterKind, + AdapterProbe, + CapabilityMap, + DebugAdapter, + Diagnostic +} from '../adapters/types' +import type { ConfigResolution, ResolvedNndConfig } from '../config' +import { formatDoctorReport } from './format' +import { runDoctor } from './doctor' + +const noCapabilities: CapabilityMap = { + http: false, + https: false, + fetch: false, + http2: false, + responseBody: false, + requestBody: false, + websocketLifecycle: false, + websocketFrames: false, + sseMessages: false, + initiator: false +} + +const legacyCapabilities: CapabilityMap = { + ...noCapabilities, + http: true, + https: true, + fetch: true, + responseBody: true, + requestBody: true +} + +function probe( + kind: AdapterKind, + available: boolean, + capabilities: CapabilityMap, + diagnostics: readonly Diagnostic[] = [], + autoSelectable = true +): AdapterProbe { + return { kind, available, autoSelectable, capabilities, diagnostics } +} + +function adapter(kind: AdapterKind, implementation: () => AdapterProbe): DebugAdapter { + return { + kind, + probe: vi.fn(implementation), + start: vi.fn(() => Promise.reject(new Error('doctor never starts adapters'))) + } +} + +function resolution(mode: ResolvedNndConfig['mode']): ConfigResolution { + return { + config: { + mode, + open: false, + wait: true, + watch: false, + runner: 'node', + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: [], + legacy: {} + }, + sources: { env: [], cli: [] } + } +} + +describe('doctor diagnostics', () => { + it('reports Auto fallback without treating the usable result as a failure', async () => { + const nativeDiagnostic: Diagnostic = { + code: 'NND_NATIVE_FLAG_REQUIRED', + level: 'error', + message: 'flag missing', + hint: 'enable it' + } + const report = await runDoctor({ + nodeVersion: '24.16.0', + packageVersion: '1.2.3', + execArgv: [], + inspectorAvailable: true, + inspector: { url: () => undefined, open: () => undefined, close: () => undefined }, + nativeAdapter: adapter('native', () => + probe('native', false, noCapabilities, [nativeDiagnostic], false) + ), + legacyAdapter: adapter('legacy', () => probe('legacy', true, legacyCapabilities)), + resolve: vi.fn(async () => resolution('auto')) + }) + + expect(report.ok).toBe(true) + expect(report.selection).toMatchObject({ requested: 'auto', selected: 'legacy' }) + expect(report.selection.fallbackReason).toMatchObject({ code: 'NND_AUTO_FALLBACK' }) + expect(report.diagnostics.map((item) => item.code)).toEqual( + expect.arrayContaining([ + 'NND_DOCTOR_NODE_VERSION', + 'NND_DOCTOR_PACKAGE_VERSION', + 'NND_DOCTOR_CONFIG_DEFAULTS', + 'NND_NATIVE_FLAG_REQUIRED', + 'NND_AUTO_FALLBACK', + 'NND_DOCTOR_SELECTED_LEGACY' + ]) + ) + }) + + it('fails forced Native selection with stable selection details', async () => { + const report = await runDoctor({ + packageVersion: '1.2.3', + nativeAdapter: adapter('native', () => probe('native', false, noCapabilities)), + legacyAdapter: adapter('legacy', () => probe('legacy', true, legacyCapabilities)), + resolve: vi.fn(async () => resolution('native')) + }) + + expect(report.ok).toBe(false) + expect(report.selection).toMatchObject({ + requested: 'native', + errorCode: 'NND_ADAPTER_UNAVAILABLE' + }) + expect(report.diagnostics.at(-1)).toMatchObject({ + code: 'NND_DOCTOR_SELECTION_FAILED', + level: 'error' + }) + }) + + it('bounded-waits and retries a forced probe', async () => { + let calls = 0 + let time = 0 + const native = adapter('native', () => { + calls += 1 + return calls <= 2 + ? probe('native', false, noCapabilities) + : probe('native', true, legacyCapabilities) + }) + const report = await runDoctor({ + packageVersion: '1.2.3', + nativeAdapter: native, + legacyAdapter: adapter('legacy', () => probe('legacy', true, legacyCapabilities)), + resolve: vi.fn(async () => resolution('native')), + probeWaitMs: 100, + probeIntervalMs: 25, + now: () => time, + sleep: vi.fn(async (milliseconds) => { + time += milliseconds + }) + }) + + expect(report.ok).toBe(true) + expect(report.selection.selected).toBe('native') + expect(calls).toBeGreaterThanOrEqual(3) + expect(time).toBeLessThanOrEqual(100) + }) + + it('waits before committing an Auto fallback when Native may become available', async () => { + let calls = 0 + let time = 0 + const native = adapter('native', () => { + calls += 1 + return calls <= 2 + ? probe('native', false, noCapabilities) + : probe('native', true, legacyCapabilities) + }) + const report = await runDoctor({ + packageVersion: '1.2.3', + nativeAdapter: native, + legacyAdapter: adapter('legacy', () => probe('legacy', true, legacyCapabilities)), + resolve: vi.fn(async () => resolution('auto')), + probeWaitMs: 100, + probeIntervalMs: 25, + now: () => time, + sleep: vi.fn(async (milliseconds) => { + time += milliseconds + }) + }) + + expect(report.ok).toBe(true) + expect(report.selection.selected).toBe('native') + expect(time).toBe(25) + }) + + it('preserves config errors in JSON and human formats', async () => { + const report = await runDoctor({ + packageVersion: '1.2.3', + nativeAdapter: adapter('native', () => probe('native', false, noCapabilities)), + legacyAdapter: adapter('legacy', () => probe('legacy', true, legacyCapabilities)), + resolve: vi.fn(async () => { + throw Object.assign(new Error('broken config'), { code: 'CUSTOM' }) + }) + }) + + expect(report.ok).toBe(false) + const json = JSON.parse(formatDoctorReport(report, true)) as typeof report + expect(json.schemaVersion).toBe(1) + expect(json.ok).toBe(false) + expect(formatDoctorReport(report)).toContain('NND_CONFIG_LOAD_FAILED') + }) +}) diff --git a/packages/network-debugger/src/diagnostics/doctor.ts b/packages/network-debugger/src/diagnostics/doctor.ts new file mode 100644 index 0000000..fb5e8d0 --- /dev/null +++ b/packages/network-debugger/src/diagnostics/doctor.ts @@ -0,0 +1,335 @@ +import { readFileSync } from 'node:fs' +import * as nodeInspector from 'node:inspector' +import { dirname, parse, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { LegacyAdapter } from '../adapters/legacy' +import { + NodeNativeAdapter, + OPTIONAL_NATIVE_NETWORK_METHODS, + REQUIRED_NATIVE_NETWORK_METHODS, + hasNativeInspectionFlag, + type NativeInspectorApi, + type NodeNativeAdapterDependencies +} from '../adapters/node-native' +import { AdapterSelector } from '../adapters/selector' +import type { AdapterProbe, DebugAdapter, Diagnostic, NetworkCapability } from '../adapters/types' +import { + NndConfigError, + resolveConfig, + type ConfigResolution, + type ResolveConfigOptions, + type ResolvedNndConfig +} from '../config' + +export const DOCTOR_SCHEMA_VERSION = 1 + +export interface DoctorNetworkMethods { + required: readonly string[] + optional: readonly string[] + available: readonly string[] + missingRequired: readonly string[] +} + +export interface DoctorSelection { + requested: ResolvedNndConfig['mode'] + selected?: 'native' | 'legacy' + fallbackReason?: Diagnostic + errorCode?: string + error?: string +} + +export interface DoctorReport { + schemaVersion: typeof DOCTOR_SCHEMA_VERSION + ok: boolean + nodeVersion: string + packageVersion: string + inspectorAvailable: boolean + experimentalFlag: boolean + networkMethods: DoctorNetworkMethods + capabilities: Readonly> + native: AdapterProbe + config?: ConfigResolution + selection: DoctorSelection + diagnostics: readonly Diagnostic[] +} + +export interface DoctorOptions { + cwd?: string + env?: NodeJS.ProcessEnv + config?: ResolveConfigOptions['cli'] + configFile?: ResolveConfigOptions['configFile'] + /** Retry a failed forced-mode probe for at most this many milliseconds. */ + probeWaitMs?: number + probeIntervalMs?: number + nodeVersion?: string + packageVersion?: string + execArgv?: readonly string[] + inspector?: NativeInspectorApi | null + inspectorAvailable?: boolean + nativeAdapter?: DebugAdapter + legacyAdapter?: DebugAdapter + resolve?: (options?: ResolveConfigOptions) => Promise + sleep?: (milliseconds: number) => Promise + now?: () => number +} + +function diagnostic( + code: string, + level: Diagnostic['level'], + message: string, + hint?: string, + details?: Readonly> +): Diagnostic { + return { + code, + level, + message, + ...(hint ? { hint } : {}), + ...(details ? { details } : {}) + } +} + +function packageVersionFrom(start: string): string | undefined { + let directory = start + const root = parse(directory).root + while (directory !== root) { + try { + const value = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8')) as { + name?: unknown + version?: unknown + } + if (value.name === 'node-network-devtools' && typeof value.version === 'string') { + return value.version + } + } catch { + // Keep walking: installed and source layouts place this module at + // different depths. + } + directory = dirname(directory) + } + return undefined +} + +export function detectPackageVersion(moduleUrl: string = import.meta.url): string { + try { + return packageVersionFrom(dirname(fileURLToPath(moduleUrl))) ?? 'unknown' + } catch { + return 'unknown' + } +} + +function methodReport(inspector: NativeInspectorApi | null): DoctorNetworkMethods { + const network = inspector?.Network + const required = [...REQUIRED_NATIVE_NETWORK_METHODS] + const optional = [...OPTIONAL_NATIVE_NETWORK_METHODS] + const available = [...required, ...optional].filter( + (method) => typeof network?.[method as keyof typeof network] === 'function' + ) + return { + required, + optional, + available, + missingRequired: required.filter((method) => !available.includes(method)) + } +} + +function configErrorDiagnostic(error: unknown): Diagnostic { + if (error instanceof NndConfigError) { + return diagnostic( + error.code, + 'error', + error.message, + 'Fix the configuration and rerun nnd doctor.', + { + ...error.details + } + ) + } + return diagnostic( + 'NND_CONFIG_LOAD_FAILED', + 'error', + error instanceof Error ? error.message : String(error), + 'Fix the configuration and rerun nnd doctor.' + ) +} + +function selectionErrorCode(error: unknown): string { + if (typeof error === 'object' && error !== null && 'code' in error) { + return String((error as { code: unknown }).code) + } + return 'NND_DOCTOR_SELECTION_FAILED' +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +interface SelectionAttempt { + native: AdapterProbe + selection: DoctorSelection + selectionDiagnostic: Diagnostic +} + +async function inspectSelection( + nativeAdapter: DebugAdapter, + legacyAdapter: DebugAdapter, + config: ResolvedNndConfig +): Promise { + const options = { + mode: config.mode, + requiredCapabilities: config.requiredCapabilities, + inspector: config.inspector + } as const + const native = await nativeAdapter.probe(options) + try { + const selected = await new AdapterSelector([nativeAdapter, legacyAdapter]).select(options) + return { + native, + selection: { + requested: config.mode, + selected: selected.adapter.kind, + ...(selected.fallbackReason ? { fallbackReason: selected.fallbackReason } : {}) + }, + selectionDiagnostic: diagnostic( + `NND_DOCTOR_SELECTED_${selected.adapter.kind.toUpperCase()}`, + selected.fallbackReason ? 'warn' : 'info', + `Selected ${selected.adapter.kind} adapter${selected.fallbackReason ? ' via Auto fallback' : ''}.`, + undefined, + { requested: config.mode, selected: selected.adapter.kind } + ) + } + } catch (error) { + const code = selectionErrorCode(error) + const message = errorMessage(error) + return { + native, + selection: { + requested: config.mode, + errorCode: code, + error: message + }, + selectionDiagnostic: diagnostic( + 'NND_DOCTOR_SELECTION_FAILED', + 'error', + message, + config.mode === 'native' + ? 'Enable the experimental flag and use a supported Node.js runtime, or choose Auto/Legacy.' + : 'Review adapter diagnostics and required capabilities.', + { code, requested: config.mode } + ) + } + } +} + +const defaultSleep = (milliseconds: number) => + new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)) + +export async function runDoctor(options: DoctorOptions = {}): Promise { + const env = options.env ?? process.env + const nodeVersion = options.nodeVersion ?? process.versions.node + const packageVersion = options.packageVersion ?? detectPackageVersion() + const execArgv = options.execArgv ?? process.execArgv + const inspector = + options.inspector === undefined + ? (nodeInspector as unknown as NativeInspectorApi) + : options.inspector + const inspectorAvailable = + options.inspectorAvailable ?? (process.features?.inspector !== false && inspector !== null) + const methods = methodReport(inspector) + const diagnostics: Diagnostic[] = [ + diagnostic('NND_DOCTOR_NODE_VERSION', 'info', `Node.js ${nodeVersion}.`, undefined, { + nodeVersion + }), + diagnostic( + 'NND_DOCTOR_PACKAGE_VERSION', + packageVersion === 'unknown' ? 'warn' : 'info', + `node-network-devtools ${packageVersion}.`, + packageVersion === 'unknown' ? 'Run doctor from an installed package.' : undefined, + { packageVersion } + ) + ] + + let resolution: ConfigResolution | undefined + try { + resolution = await (options.resolve ?? resolveConfig)({ + cwd: options.cwd, + env, + cli: options.config, + configFile: options.configFile + }) + diagnostics.push( + resolution.sources.configFile + ? diagnostic( + 'NND_DOCTOR_CONFIG_LOADED', + 'info', + `Loaded configuration from ${resolution.sources.configFile}.`, + undefined, + { sources: resolution.sources } + ) + : diagnostic( + 'NND_DOCTOR_CONFIG_DEFAULTS', + 'info', + 'No config file found; using environment and defaults.', + undefined, + { sources: resolution.sources } + ) + ) + } catch (error) { + diagnostics.push(configErrorDiagnostic(error)) + } + + const effectiveConfig: ResolvedNndConfig = resolution?.config ?? { + mode: options.config?.mode ?? 'auto', + open: false, + wait: true, + watch: false, + runner: 'node', + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: options.config?.requiredCapabilities ?? [], + legacy: {} + } + const nativeDependencies: NodeNativeAdapterDependencies = { + inspector, + inspectorAvailable, + execArgv, + nodeVersion + } + const nativeAdapter = options.nativeAdapter ?? new NodeNativeAdapter(nativeDependencies) + const legacyAdapter = options.legacyAdapter ?? new LegacyAdapter() + const sleep = options.sleep ?? defaultSleep + const now = options.now ?? Date.now + const waitMs = Math.max(0, options.probeWaitMs ?? 0) + const interval = Math.max(1, options.probeIntervalMs ?? 100) + const deadline = now() + waitMs + let attempt = await inspectSelection(nativeAdapter, legacyAdapter, effectiveConfig) + + const shouldRetry = () => + !attempt.selection.selected || + (effectiveConfig.mode === 'auto' && + attempt.selection.selected === 'legacy' && + attempt.selection.fallbackReason !== undefined) + + while (shouldRetry() && now() < deadline) { + await sleep(Math.min(interval, Math.max(0, deadline - now()))) + attempt = await inspectSelection(nativeAdapter, legacyAdapter, effectiveConfig) + } + + diagnostics.push(...attempt.native.diagnostics) + if (attempt.selection.fallbackReason) diagnostics.push(attempt.selection.fallbackReason) + diagnostics.push(attempt.selectionDiagnostic) + + return { + schemaVersion: DOCTOR_SCHEMA_VERSION, + ok: Boolean(resolution && attempt.selection.selected), + nodeVersion, + packageVersion, + inspectorAvailable, + experimentalFlag: hasNativeInspectionFlag(execArgv), + networkMethods: methods, + capabilities: attempt.native.capabilities, + native: attempt.native, + ...(resolution ? { config: resolution } : {}), + selection: attempt.selection, + diagnostics + } +} diff --git a/packages/network-debugger/src/diagnostics/format.ts b/packages/network-debugger/src/diagnostics/format.ts new file mode 100644 index 0000000..da1f671 --- /dev/null +++ b/packages/network-debugger/src/diagnostics/format.ts @@ -0,0 +1,33 @@ +import type { Diagnostic } from '../adapters/types' +import type { DoctorReport } from './doctor' + +function mark(level: Diagnostic['level']): string { + if (level === 'error') return 'x' + if (level === 'warn') return '!' + return 'i' +} + +export function formatDoctorReport(report: DoctorReport, json = false): string { + if (json) return `${JSON.stringify(report, null, 2)}\n` + + const selected = report.selection.selected ?? 'none' + const capabilities = Object.entries(report.capabilities) + .filter(([, supported]) => supported) + .map(([capability]) => capability) + const lines = [ + `Node Network Devtools doctor (${report.ok ? 'ok' : 'failed'})`, + `Node: ${report.nodeVersion}`, + `Package: ${report.packageVersion}`, + `Inspector: ${report.inspectorAvailable ? 'available' : 'unavailable'}`, + `Experimental flag: ${report.experimentalFlag ? 'enabled' : 'missing'}`, + `Network methods: ${report.networkMethods.available.length} available, ${report.networkMethods.missingRequired.length} required missing`, + `Selection: ${report.selection.requested} -> ${selected}`, + `Native capabilities: ${capabilities.length ? capabilities.join(', ') : 'none'}`, + '', + ...report.diagnostics.map((item) => { + const hint = item.hint ? ` Hint: ${item.hint}` : '' + return `[${mark(item.level)}] ${item.code}: ${item.message}${hint}` + }) + ] + return `${lines.join('\n')}\n` +} diff --git a/packages/network-debugger/src/diagnostics/index.ts b/packages/network-debugger/src/diagnostics/index.ts new file mode 100644 index 0000000..9fca59d --- /dev/null +++ b/packages/network-debugger/src/diagnostics/index.ts @@ -0,0 +1,2 @@ +export * from './doctor' +export * from './format' diff --git a/packages/network-debugger/src/fork/devtool/index.test.ts b/packages/network-debugger/src/fork/devtool/index.test.ts index ca5a2bf..01cebc3 100644 --- a/packages/network-debugger/src/fork/devtool/index.test.ts +++ b/packages/network-debugger/src/fork/devtool/index.test.ts @@ -1,533 +1,371 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { - mockWsServerInstance, - mockWsSocketInstance, - mockOpen, - mockWsDebuggerInstance, - wsServerHandlers, - wsSocketHandlers, - wsDebuggerHandlers, - MockServer, - MockWebSocket -} = vi.hoisted(() => { - const wsServerHandlers = new Map void)[]>() - const wsSocketHandlers = new Map void)[]>() - const wsDebuggerHandlers = new Map void)[]>() - - const mockWsServerInstance = { - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - if (!wsServerHandlers.has(event)) { - wsServerHandlers.set(event, []) +import { once } from 'node:events' +import { setTimeout as delay } from 'node:timers/promises' +import { afterEach, describe, expect, test } from 'vitest' +import { WebSocket } from 'ws' +import { + CDP_ERROR_CODES, + DevtoolServer, + DevtoolServerClosedError, + MAX_BUFFERED_EVENT_BYTES, + MAX_BUFFERED_EVENTS +} from './index' + +type JsonMessage = Record + +class ProtocolClient { + readonly socket: WebSocket + private readonly messages: JsonMessage[] = [] + private readonly waiters: Array<{ + predicate(message: JsonMessage): boolean + resolve(message: JsonMessage): void + reject(error: Error): void + timer: ReturnType + }> = [] + + private constructor(url: string) { + this.socket = new WebSocket(url) + this.socket.on('message', (raw) => { + const message = JSON.parse(raw.toString()) as JsonMessage + const waiterIndex = this.waiters.findIndex((waiter) => waiter.predicate(message)) + if (waiterIndex >= 0) { + const [waiter] = this.waiters.splice(waiterIndex, 1) + clearTimeout(waiter.timer) + waiter.resolve(message) + } else { + this.messages.push(message) } - wsServerHandlers.get(event)!.push(handler) - }), - close: vi.fn() - } - - const mockWsSocketInstance = { - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - if (!wsSocketHandlers.has(event)) { - wsSocketHandlers.set(event, []) - } - wsSocketHandlers.get(event)!.push(handler) - }), - send: vi.fn(), - close: vi.fn() + }) } - const mockWsDebuggerInstance = { - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - if (!wsDebuggerHandlers.has(event)) { - wsDebuggerHandlers.set(event, []) - } - wsDebuggerHandlers.get(event)!.push(handler) - }), - send: vi.fn(), - close: vi.fn() + static async connect(url: string) { + const client = new ProtocolClient(url) + await once(client.socket, 'open') + return client } - // 使用 class 语法创建 mock 构造函数 - class MockServer { - constructor() { - return mockWsServerInstance - } + send(message: unknown) { + this.socket.send(JSON.stringify(message)) } - class MockWebSocket { - constructor() { - return mockWsDebuggerInstance - } + sendRaw(message: string) { + this.socket.send(message) } - const mockOpen = vi.fn() - - return { - mockWsServerInstance, - mockWsSocketInstance, - mockOpen, - mockWsDebuggerInstance, - wsServerHandlers, - wsSocketHandlers, - wsDebuggerHandlers, - MockServer, - MockWebSocket + next( + predicate: (message: JsonMessage) => boolean = () => true, + timeoutMs = 2_000 + ): Promise { + const index = this.messages.findIndex(predicate) + if (index >= 0) return Promise.resolve(this.messages.splice(index, 1)[0]) + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + const waiterIndex = this.waiters.findIndex((waiter) => waiter.timer === timer) + if (waiterIndex >= 0) this.waiters.splice(waiterIndex, 1) + reject(new Error('Timed out waiting for a CDP message.')) + }, timeoutMs) + this.waiters.push({ predicate, resolve, reject, timer }) + }) } -}) -// Mock ws 模块 -vi.mock('ws', () => { - return { - Server: MockServer, - WebSocket: MockWebSocket + async close() { + if (this.socket.readyState === WebSocket.CLOSED) return + const closed = once(this.socket, 'close') + this.socket.close() + await closed } +} + +const servers = new Set() +const clients = new Set() + +async function createServer(options: Partial[0]> = {}) { + const server = new DevtoolServer({ port: 0, ...options }) + servers.add(server) + const target = await server.ready + return { server, target } +} + +async function connect(url: string) { + const client = await ProtocolClient.connect(url) + clients.add(client) + return client +} + +afterEach(async () => { + await Promise.all([...clients].map((client) => client.close().catch(() => undefined))) + clients.clear() + await Promise.all([...servers].map((server) => server.close())) + servers.clear() }) -// Mock open 模块 -vi.mock('open', () => { - return { - default: mockOpen, - apps: { - chrome: 'google-chrome' - } - } -}) +describe('Legacy discoverable CDP target', () => { + test('binds port 0 before publishing one consistent loopback target', async () => { + const server = new DevtoolServer({ port: 0 }) + servers.add(server) + expect(server.target).toBeUndefined() + + const target = await server.ready + const discovery = new URL(target.discoveryUrl) + expect(discovery.hostname).toBe('127.0.0.1') + expect(Number(discovery.port)).toBeGreaterThan(0) + expect(target.webSocketDebuggerUrl).toMatch( + new RegExp(`^ws://127\\.0\\.0\\.1:${discovery.port}/devtools/page/${target.id}$`) + ) + expect(server.target).toEqual(target) + + const list = await fetch(target.discoveryUrl).then((response) => response.json()) + const alias = await fetch(new URL('/json', target.discoveryUrl)).then((response) => + response.json() + ) + const version = await fetch(new URL('/json/version', target.discoveryUrl)).then((response) => + response.json() + ) + const protocol = await fetch(new URL('/json/protocol', target.discoveryUrl)).then((response) => + response.json() + ) + + expect(list).toEqual([target]) + expect(alias).toEqual([target]) + expect(version).toMatchObject({ + Browser: 'node-network-devtools/2', + 'Protocol-Version': '1.3', + webSocketDebuggerUrl: target.webSocketDebuggerUrl + }) + expect(protocol.version).toEqual({ major: '1', minor: '3' }) + expect(protocol.domains.map((domain: { domain: string }) => domain.domain)).toEqual( + expect.arrayContaining(['Network', 'Debugger', 'Runtime', 'Schema']) + ) -// Mock common 模块 -vi.mock('../../common', () => ({ - IS_DEV_MODE: false, - REMOTE_DEBUGGER_PORT: 9333 -})) - -// Mock utils 模块 -vi.mock('../../utils', () => ({ - log: vi.fn() -})) - -describe('fork/devtool/index.ts', () => { - beforeEach(() => { - vi.clearAllMocks() - wsServerHandlers.clear() - wsSocketHandlers.clear() - wsDebuggerHandlers.clear() + const notFound = await fetch(new URL('/not-a-target', target.discoveryUrl)) + expect(notFound.status).toBe(404) }) - afterEach(() => { - vi.restoreAllMocks() + test('accepts upgrades only on the published WebSocket path', async () => { + const { target } = await createServer() + const wrongUrl = new URL(target.webSocketDebuggerUrl) + wrongUrl.pathname = '/devtools/page/wrong-target' + const socket = new WebSocket(wrongUrl) + const outcome = Promise.race([ + once(socket, 'open').then(() => 'open'), + once(socket, 'error').then(() => 'error'), + once(socket, 'unexpected-response').then(() => 'rejected') + ]) + await expect(outcome).resolves.not.toBe('open') + socket.terminate() }) - describe('DevtoolServer 类', () => { - describe('构造函数', () => { - test('创建 WebSocket Server 并监听指定端口', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - expect(server).toBeDefined() - }) - - test('监听 listening 事件', async () => { - const { DevtoolServer } = await import('./index') - - new DevtoolServer({ port: 5271 }) - - expect(mockWsServerInstance.on).toHaveBeenCalledWith('listening', expect.any(Function)) - }) - - test('监听 connection 事件', async () => { - const { DevtoolServer } = await import('./index') - - new DevtoolServer({ port: 5271 }) - - expect(mockWsServerInstance.on).toHaveBeenCalledWith('connection', expect.any(Function)) - }) - - test('autoOpenDevtool 默认为 true', async () => { - const { DevtoolServer } = await import('./index') - - new DevtoolServer({ port: 5271 }) - - // 触发 listening 事件 - const listeningHandler = wsServerHandlers.get('listening')?.[0] - if (listeningHandler) { - listeningHandler() - } - - // 由于 autoOpenDevtool 默认为 true,应该调用 open - // 但由于 IS_DEV_MODE 被 mock 为 false,会尝试打开浏览器 - expect(mockOpen).toHaveBeenCalled() - }) - - test('autoOpenDevtool 为 false 时不自动打开', async () => { - vi.clearAllMocks() - wsServerHandlers.clear() - - const { DevtoolServer } = await import('./index') - - new DevtoolServer({ port: 5271, autoOpenDevtool: false }) - - // 触发 listening 事件 - const listeningHandler = wsServerHandlers.get('listening')?.[0] - if (listeningHandler) { - listeningHandler() - } - - // autoOpenDevtool 为 false,不应该调用 open - expect(mockOpen).not.toHaveBeenCalled() - }) - - test('onConnect 回调在连接时被调用', async () => { - const { DevtoolServer } = await import('./index') - const onConnect = vi.fn() - - new DevtoolServer({ port: 5271, onConnect }) - - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - expect(onConnect).toHaveBeenCalled() - }) - - test('onClose 回调在连接关闭时被调用', async () => { - const { DevtoolServer } = await import('./index') - const onClose = vi.fn() - - new DevtoolServer({ port: 5271, onClose }) - - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - // 触发 socket close 事件 - const closeHandler = wsSocketHandlers.get('close')?.[0] - if (closeHandler) { - closeHandler() - } + test('uses a parent-supplied stable target identity in discovery and WebSocket URLs', async () => { + const { target } = await createServer({ targetId: 'stable.parent-target_1' }) + expect(target.id).toBe('stable.parent-target_1') + expect(new URL(target.webSocketDebuggerUrl).pathname).toBe( + '/devtools/page/stable.parent-target_1' + ) + }) - expect(onClose).toHaveBeenCalled() - }) + test('single-casts same-id async command responses to their source clients', async () => { + const { server, target } = await createServer() + server.on(async (_error, message, context) => { + if (!message || !('method' in message) || message.method !== 'Test.echo' || !context) { + return false + } + const token = String(message.params?.token) + if (token === 'slow') await delay(30) + await context.result({ token }) + return true }) - describe('消息处理', () => { - test('接收到消息时通知所有监听器', async () => { - const { DevtoolServer } = await import('./index') - const listener = vi.fn() - - const server = new DevtoolServer({ port: 5271 }) - server.on(listener) + const first = await connect(target.webSocketDebuggerUrl) + const second = await connect(target.webSocketDebuggerUrl) + first.send({ id: 7, method: 'Test.echo', params: { token: 'slow' } }) + second.send({ id: 7, method: 'Test.echo', params: { token: 'fast' } }) - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - // 触发 message 事件 - const messageHandler = wsSocketHandlers.get('message')?.[0] - const testMessage = { method: 'Network.enable', params: {} } - if (messageHandler) { - messageHandler(Buffer.from(JSON.stringify(testMessage))) - } - - expect(listener).toHaveBeenCalledWith(null, testMessage) - }) - - test('接收到错误时通知所有监听器', async () => { - const { DevtoolServer } = await import('./index') - const listener = vi.fn() - - const server = new DevtoolServer({ port: 5271 }) - server.on(listener) - - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - // 触发 error 事件 - const errorHandler = wsSocketHandlers.get('error')?.[0] - const testError = new Error('WebSocket error') - if (errorHandler) { - errorHandler(testError) - } - - expect(listener).toHaveBeenCalledWith(testError) - }) + await expect(second.next((message) => message.id === 7)).resolves.toEqual({ + id: 7, + result: { token: 'fast' } }) - - describe('send 方法', () => { - test('发送消息到 WebSocket', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - // 触发 connection 事件以建立连接 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - const message = { method: 'Network.requestWillBeSent', params: { requestId: '1' } } - await server.send(message) - - expect(mockWsSocketInstance.send).toHaveBeenCalledWith(JSON.stringify(message)) - }) - - test('发送响应消息', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - const response = { id: '1', result: { body: 'test' } } - await server.send(response) - - expect(mockWsSocketInstance.send).toHaveBeenCalledWith(JSON.stringify(response)) - }) - - test('发送错误响应消息', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - // 触发 connection 事件 - const connectionHandler = wsServerHandlers.get('connection')?.[0] - if (connectionHandler) { - connectionHandler(mockWsSocketInstance) - } - - const errorResponse = { id: '2', error: { code: -32601, message: 'Method not found' } } - await server.send(errorResponse) - - expect(mockWsSocketInstance.send).toHaveBeenCalledWith(JSON.stringify(errorResponse)) - }) + await expect(first.next((message) => message.id === 7)).resolves.toEqual({ + id: 7, + result: { token: 'slow' } }) + }) - describe('close 方法', () => { - test('关闭 WebSocket Server', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - server.close() - - expect(mockWsServerInstance.close).toHaveBeenCalled() - }) - - test('关闭浏览器进程(如果存在)', async () => { - const { DevtoolServer } = await import('./index') - - const mockBrowserProcess = { kill: vi.fn() } - mockOpen.mockResolvedValue(mockBrowserProcess) - - const server = new DevtoolServer({ port: 5271 }) - - // 触发 listening 事件以打开浏览器 - const listeningHandler = wsServerHandlers.get('listening')?.[0] - if (listeningHandler) { - listeningHandler() - } - - // 等待 open 完成 - await vi.waitFor(() => { - expect(mockOpen).toHaveBeenCalled() - }) - - // 等待异步操作完成 - await new Promise((resolve) => setTimeout(resolve, 0)) - - server.close() - - expect(mockWsServerInstance.close).toHaveBeenCalled() - }) + test('preserves legacy devtool.send response routing across async handler work', async () => { + const { server, target } = await createServer() + server.on(async (_error, message) => { + if (!message || !('method' in message) || message.method !== 'Test.legacyReply') return false + await delay(Number(message.params?.delay ?? 0)) + await server.send({ id: message.id!, result: { owner: message.params?.owner } }) + return true }) + const first = await connect(target.webSocketDebuggerUrl) + const second = await connect(target.webSocketDebuggerUrl) - describe('open 方法', () => { - test('在开发模式下不打开浏览器', async () => { - // 重新 mock common 模块为开发模式 - vi.doMock('../../common', () => ({ - IS_DEV_MODE: true, - REMOTE_DEBUGGER_PORT: 9333 - })) - - vi.clearAllMocks() - - // 重新导入模块 - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271, autoOpenDevtool: false }) - await server.open() - - // 在开发模式下不应该调用 open - // 注意:由于模块缓存,这个测试可能需要特殊处理 - }) - - test('在非开发模式下打开 Chrome DevTools', async () => { - vi.clearAllMocks() + first.send({ id: 'same', method: 'Test.legacyReply', params: { owner: 'first', delay: 20 } }) + second.send({ id: 'same', method: 'Test.legacyReply', params: { owner: 'second' } }) - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271, autoOpenDevtool: false }) - await server.open() - - expect(mockOpen).toHaveBeenCalledWith( - expect.stringContaining('devtools://devtools/bundled/inspector.html'), - expect.objectContaining({ - app: expect.objectContaining({ - name: 'google-chrome' - }), - wait: true - }) - ) - }) - - test('打开失败时输出警告但不抛出错误', async () => { - vi.clearAllMocks() - - mockOpen.mockRejectedValue(new Error('Failed to open')) - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271, autoOpenDevtool: false }) - await server.open() - - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Open devtools failed')) - - consoleWarnSpy.mockRestore() - }) + expect(await second.next((message) => message.id === 'same')).toEqual({ + id: 'same', + result: { owner: 'second' } }) + expect(await first.next((message) => message.id === 'same')).toEqual({ + id: 'same', + result: { owner: 'first' } + }) + }) - describe('继承 BaseDevtoolServer', () => { - test('继承 timestamp 属性', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - expect(server.timestamp).toBe(0) - }) - - test('继承 getTimestamp 方法', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - expect(typeof server.getTimestamp).toBe('function') - expect(typeof server.getTimestamp()).toBe('number') - }) - - test('继承 updateTimestamp 方法', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - expect(typeof server.updateTimestamp).toBe('function') - }) - - test('继承 on 方法', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - const listener = vi.fn() - - server.on(listener) - - expect(server.listeners).toContain(listener) - }) + test('broadcasts events to every connected frontend', async () => { + const { server, target } = await createServer() + const first = await connect(target.webSocketDebuggerUrl) + const second = await connect(target.webSocketDebuggerUrl) + first.send({ id: 1, method: 'Network.enable' }) + second.send({ id: 2, method: 'Network.enable' }) + await first.next((message) => message.id === 1) + await second.next((message) => message.id === 2) + const event = { + method: 'Network.requestWillBeSent', + params: { requestId: 'broadcast-request' } + } - test('继承 listeners 属性', async () => { - const { DevtoolServer } = await import('./index') + await server.send(event) + await expect(first.next((message) => message.method === event.method)).resolves.toEqual(event) + await expect(second.next((message) => message.method === event.method)).resolves.toEqual(event) + }) - const server = new DevtoolServer({ port: 5271 }) + test('sends Network events only to clients that enabled the domain', async () => { + const { server, target } = await createServer() + const enabled = await connect(target.webSocketDebuggerUrl) + const disabled = await connect(target.webSocketDebuggerUrl) + enabled.send({ id: 'enable', method: 'Network.enable' }) + await enabled.next((message) => message.id === 'enable') - expect(Array.isArray(server.listeners)).toBe(true) - }) + await server.send({ + method: 'Network.loadingFinished', + params: { requestId: 'enabled-only' } }) + expect( + await enabled.next((message) => message.params?.requestId === 'enabled-only') + ).toMatchObject({ + method: 'Network.loadingFinished' + }) + await expect( + disabled.next((message) => message.params?.requestId === 'enabled-only', 100) + ).rejects.toThrow('Timed out') + + enabled.send({ id: 'disable', method: 'Network.disable' }) + await enabled.next((message) => message.id === 'disable') + await server.send({ method: 'Network.loadingFinished', params: { requestId: 'disabled-now' } }) + await expect( + enabled.next((message) => message.params?.requestId === 'disabled-now', 100) + ).rejects.toThrow('Timed out') + }, 6_000) + + test('returns standard results/errors for zero, string, unknown, invalid and thrown commands', async () => { + const { server, target } = await createServer() + server.on(async (_error, message, context) => { + if (!message || !('method' in message) || !context) return false + if (message.method === 'Test.fail') throw new Error('handler exploded') + if (message.method === 'Test.applicationError') { + await context.error(CDP_ERROR_CODES.SERVER_ERROR, 'request body unavailable') + return true + } + return false + }) + const client = await connect(target.webSocketDebuggerUrl) - describe('IDevtoolServer 接口实现', () => { - test('实现 send 方法', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271 }) - - expect(typeof server.send).toBe('function') - }) - - test('实现 close 方法', async () => { - const { DevtoolServer } = await import('./index') + client.send({ id: 0, method: 'Network.enable', params: {} }) + expect(await client.next((message) => message.id === 0)).toEqual({ id: 0, result: {} }) - const server = new DevtoolServer({ port: 5271 }) + client.send({ id: 'debugger', method: 'Debugger.enable', params: {} }) + expect(await client.next((message) => message.id === 'debugger')).toMatchObject({ + id: 'debugger', + result: { debuggerId: target.id } + }) - expect(typeof server.close).toBe('function') - }) + client.send({ id: 2, method: 'Missing.command', params: {} }) + expect(await client.next((message) => message.id === 2)).toMatchObject({ + id: 2, + error: { code: CDP_ERROR_CODES.METHOD_NOT_FOUND } + }) - test('实现 open 方法', async () => { - const { DevtoolServer } = await import('./index') + client.send({ id: 3, method: 'Network.enable', params: [] }) + expect(await client.next((message) => message.id === 3)).toMatchObject({ + id: 3, + error: { code: CDP_ERROR_CODES.INVALID_PARAMS } + }) - const server = new DevtoolServer({ port: 5271 }) + client.send({ id: 4, method: 'Test.fail' }) + expect(await client.next((message) => message.id === 4)).toMatchObject({ + id: 4, + error: { code: CDP_ERROR_CODES.INTERNAL_ERROR, message: 'handler exploded' } + }) - expect(typeof server.open).toBe('function') - }) + client.send({ id: 5, method: 'Test.applicationError' }) + expect(await client.next((message) => message.id === 5)).toMatchObject({ + id: 5, + error: { code: CDP_ERROR_CODES.SERVER_ERROR, message: 'request body unavailable' } }) }) - describe('DevtoolServerInitOptions 接口', () => { - test('port 是必需的', async () => { - const { DevtoolServer } = await import('./index') + test('survives malformed JSON and continues serving commands', async () => { + const { target } = await createServer() + const client = await connect(target.webSocketDebuggerUrl) + client.sendRaw('{ definitely not json') - // 这个测试主要验证类型,运行时只需确保可以创建实例 - const server = new DevtoolServer({ port: 5271 }) - expect(server).toBeDefined() + expect(await client.next((message) => message.id === null)).toMatchObject({ + error: { code: CDP_ERROR_CODES.INVALID_REQUEST } }) - test('autoOpenDevtool 是可选的', async () => { - const { DevtoolServer } = await import('./index') - - const server1 = new DevtoolServer({ port: 5271 }) - const server2 = new DevtoolServer({ port: 5272, autoOpenDevtool: true }) - const server3 = new DevtoolServer({ port: 5273, autoOpenDevtool: false }) - - expect(server1).toBeDefined() - expect(server2).toBeDefined() - expect(server3).toBeDefined() - }) + client.send({ id: 9, method: 'Runtime.enable' }) + expect(await client.next((message) => message.id === 9)).toEqual({ id: 9, result: {} }) + }) - test('onConnect 是可选的', async () => { - const { DevtoolServer } = await import('./index') + test('bounds no-client history and replays it after enable on reconnect', async () => { + const { server, target } = await createServer() + for (let index = 0; index < MAX_BUFFERED_EVENTS + 50; index += 1) { + await server.send({ + method: 'Network.requestWillBeSent', + params: { requestId: `request-${index}` } + }) + } + expect(server.bufferedEventCount).toBe(MAX_BUFFERED_EVENTS) + expect(server.bufferedEventBytes).toBeLessThanOrEqual(MAX_BUFFERED_EVENT_BYTES) + expect(server.clientCount).toBe(0) + + const first = await connect(target.webSocketDebuggerUrl) + first.send({ id: 1, method: 'Network.enable' }) + expect(await first.next((message) => message.id === 1)).toEqual({ id: 1, result: {} }) + expect(await first.next((message) => message.params?.requestId === 'request-50')).toMatchObject( + { + method: 'Network.requestWillBeSent' + } + ) + await first.close() + clients.delete(first) - const server = new DevtoolServer({ port: 5271, onConnect: () => {} }) - expect(server).toBeDefined() + await server.send({ + method: 'Network.loadingFinished', + params: { requestId: 'after-refresh' } }) - - test('onClose 是可选的', async () => { - const { DevtoolServer } = await import('./index') - - const server = new DevtoolServer({ port: 5271, onClose: () => {} }) - expect(server).toBeDefined() + const refreshed = await connect(target.webSocketDebuggerUrl) + refreshed.send({ id: 'enable-again', method: 'Network.enable' }) + expect(await refreshed.next((message) => message.id === 'enable-again')).toEqual({ + id: 'enable-again', + result: {} }) + expect( + await refreshed.next((message) => message.params?.requestId === 'after-refresh') + ).toMatchObject({ method: 'Network.loadingFinished' }) }) - describe('模块导出', () => { - test('导出 DevtoolServer 类', async () => { - const module = await import('./index') - expect(module.DevtoolServer).toBeDefined() - }) + test('close rejects an explicit pending client waiter, clears history and releases the port', async () => { + const { server, target } = await createServer() + await server.send({ method: 'Network.loadingFinished', params: { requestId: 'buffered' } }) + const pending = server.waitForClient() + const discoveryUrl = target.discoveryUrl - test('重新导出 type.ts 中的类型', async () => { - const module = await import('./index') - // BaseDevtoolServer 应该通过 export * from './type' 导出 - expect(module.BaseDevtoolServer).toBeDefined() - }) + await server.close() + await expect(pending).rejects.toBeInstanceOf(DevtoolServerClosedError) + expect(server.bufferedEventCount).toBe(0) + await expect(fetch(discoveryUrl)).rejects.toThrow() }) }) diff --git a/packages/network-debugger/src/fork/devtool/index.ts b/packages/network-debugger/src/fork/devtool/index.ts index 62c2fbb..f2d13fa 100644 --- a/packages/network-debugger/src/fork/devtool/index.ts +++ b/packages/network-debugger/src/fork/devtool/index.ts @@ -1,141 +1,737 @@ -import { Server, WebSocket } from 'ws' -import open, { apps } from 'open' -import { type ChildProcess } from 'child_process' -import { IS_DEV_MODE } from '../../common' -import { REMOTE_DEBUGGER_PORT } from '../../common' +import { AsyncLocalStorage } from 'node:async_hooks' +import { randomUUID } from 'node:crypto' +import { createServer, type Server as HttpServer, type ServerResponse } from 'node:http' +import type { AddressInfo } from 'node:net' +import type { Duplex } from 'node:stream' +import { WebSocket, WebSocketServer, type RawData } from 'ws' +import type { DevtoolsTarget } from '../../adapters/types' +import type { CdpId } from '../../legacy-bridge/contracts' import { log } from '../../utils' -import { BaseDevtoolServer, DevtoolMessage } from './type' +import { + BaseDevtoolServer, + type DevtoolCommandContext, + type DevtoolErrorResponse, + type DevtoolMessage, + type DevtoolMessageRequest, + type DevtoolMessageResponse +} from './type' + +const LOOPBACK_HOST = '127.0.0.1' +const PROTOCOL_VERSION = '1.3' +export const MAX_BUFFERED_EVENTS = 1_000 +export const MAX_BUFFERED_EVENT_BYTES = 10 * 1024 * 1024 + +export const CDP_ERROR_CODES = Object.freeze({ + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32603, + SERVER_ERROR: -32000 +}) export interface DevtoolServerInitOptions { + /** Bind port. `0` asks the operating system for a free port. */ port: number + /** Only 127.0.0.1 is accepted; the endpoint is never exposed remotely. */ + host?: '127.0.0.1' + /** Stable identity supplied by the parent so a restarted child keeps its URL. */ + targetId?: string + title?: string + /** @deprecated Browser launching is owned by the caller. */ autoOpenDevtool?: boolean onConnect?: () => void onClose?: () => void } export interface IDevtoolServer { - send(message: DevtoolMessage): Promise - close(): void - open(): Promise + readonly ready?: Promise + readonly target?: DevtoolsTarget + send(message: DevtoolMessage, client?: WebSocket): Promise + close(): Promise | void + open(): Promise +} + +export class DevtoolServerClosedError extends Error { + readonly code = 'NND_LEGACY_TARGET_CLOSED' + + constructor() { + super('Legacy DevTools target closed before a frontend connection became available.') + this.name = 'DevtoolServerClosedError' + } } + export * from './type' +interface CommandScope { + client: WebSocket + id: CdpId + responded: boolean +} + +interface ClientWaiter { + resolve(client: WebSocket): void + reject(error: Error): void +} + +interface BufferedEvent { + sequence: number + method: string + serialized: string + bytes: number +} + +type BuiltinResult = Record | void + +/** + * A loopback-only, discoverable CDP target used by the Legacy adapter. + * Events fan out to every frontend while command responses are single-cast. + */ export class DevtoolServer extends BaseDevtoolServer implements IDevtoolServer { - private server: Server - private port: number - private browser: ChildProcess | null = null - private socket: Promise<[WebSocket]> + readonly ready: Promise - constructor(props: DevtoolServerInitOptions) { + private readonly host = LOOPBACK_HOST + private readonly port: number + private readonly title: string + private readonly targetId: string + private readonly webSocketPath: string + private readonly onConnect?: () => void + private readonly onClose?: () => void + private readonly commandScope = new AsyncLocalStorage() + private readonly clientWaiters = new Set() + private readonly networkEnabledClients = new WeakSet() + private readonly networkReplayingClients = new WeakSet() + private readonly networkReplayCursor = new WeakMap() + private readonly builtinCommands = this.createBuiltinCommands() + private readonly eventHistory: BufferedEvent[] = [] + + private httpServer?: HttpServer + private webSocketServer?: WebSocketServer + private startPromise?: Promise + private closePromise?: Promise + private rejectStart?: (error: Error) => void + private _target?: DevtoolsTarget + private closed = false + private eventSequence = 0 + private eventHistoryBytes = 0 + + constructor(options: DevtoolServerInitOptions) { super() - const { port, autoOpenDevtool = true, onConnect, onClose } = props + const port = options.port ?? 0 + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + throw new RangeError(`Invalid Legacy target port: ${String(port)}`) + } + if (options.host !== undefined && options.host !== LOOPBACK_HOST) { + throw new Error(`Legacy target host must be ${LOOPBACK_HOST}.`) + } + if (options.targetId !== undefined && !/^[A-Za-z0-9._-]+$/.test(options.targetId)) { + throw new Error( + 'Legacy targetId may contain only letters, numbers, dot, underscore and dash.' + ) + } + this.port = port - this.server = new Server({ port }) - const { server } = this + this.targetId = options.targetId ?? `node-network-devtools-${randomUUID()}` + this.title = options.title ?? 'Node Network Devtools (Legacy)' + this.webSocketPath = `/devtools/page/${this.targetId}` + this.onConnect = options.onConnect + this.onClose = options.onClose + this.ready = this.open() + } - server.on('listening', () => { - log(`devtool server is listening on port ${port}`) - autoOpenDevtool && this.open() + get target(): DevtoolsTarget | undefined { + return this._target + } + + get clientCount(): number { + return this.webSocketServer?.clients.size ?? 0 + } + + get bufferedEventCount(): number { + return this.eventHistory.length + } + + get bufferedEventBytes(): number { + return this.eventHistoryBytes + } + + public open(): Promise { + if (this.startPromise) return this.startPromise + if (this.closed) return Promise.reject(new DevtoolServerClosedError()) + + const webSocketServer = new WebSocketServer({ noServer: true }) + const httpServer = createServer((request, response) => { + this.handleHttpRequest(request.url, response) }) + this.webSocketServer = webSocketServer + this.httpServer = httpServer - this.socket = new Promise<[WebSocket]>((resolve) => { - server.on('connection', (socket) => { - onConnect?.() - this.socket.then((l) => { - l[0] = socket - }) - log('devtool connected') - socket.on('message', (message) => { - const msg = JSON.parse(message.toString()) - this.listeners.forEach((listener) => listener(null, msg)) - }) - socket.on('close', () => { - log('devtool closed') - onClose?.() - }) - socket.on('error', (error) => { - this.listeners.forEach((listener) => listener(error)) - }) - resolve([socket] satisfies [WebSocket]) + httpServer.on('upgrade', (request, socket, head) => { + if (this.getPathname(request.url) !== this.webSocketPath) { + this.rejectUpgrade(socket) + return + } + webSocketServer.handleUpgrade(request, socket, head, (client) => { + webSocketServer.emit('connection', client, request) }) }) + + webSocketServer.on('connection', (client) => this.attachClient(client)) + webSocketServer.on('error', (error) => this.notifyError(error)) + + this.startPromise = new Promise((resolve, reject) => { + let settled = false + this.rejectStart = (error) => { + if (settled) return + settled = true + reject(error) + } + + httpServer.once('listening', () => { + if (settled) return + const address = httpServer.address() + if (!address || typeof address === 'string') { + const error = new Error('Legacy target did not expose a TCP address.') + settled = true + reject(error) + return + } + const target = this.createTarget(address) + this._target = target + settled = true + log(`legacy target is listening at ${target.discoveryUrl}`) + resolve(target) + }) + + httpServer.on('error', (error) => { + this.notifyError(error) + this.rejectStart?.(error) + }) + + try { + httpServer.listen(this.port, this.host) + } catch (error) { + const normalized = error instanceof Error ? error : new Error(String(error)) + this.notifyError(normalized) + this.rejectStart?.(normalized) + } + }) + + return this.startPromise } - public async open() { - const url = `devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${this.port}` - try { - if (IS_DEV_MODE) { - log(`In dev mode, open chrome devtool manually: ${url}`) + /** Broadcast events; route responses only to their source connection. */ + public async send(message: DevtoolMessage, client?: WebSocket): Promise { + if (this.closed) throw new DevtoolServerClosedError() + + if (this.isResponse(message)) { + const scope = this.commandScope.getStore() + if (client) { + if (scope && client === scope.client && message.id === scope.id) scope.responded = true + await this.sendJson(client, message) + return + } + if (scope && message.id === scope.id) { + scope.responded = true + await this.sendJson(scope.client, message) return } - const pro = await open(url, { - app: { - name: apps.chrome, - arguments: [ - process.platform !== 'darwin' ? `--remote-debugging-port=${REMOTE_DEBUGGER_PORT}` : '' - ] - }, - wait: true + const clients = this.liveClients() + if (clients.length !== 1) { + throw new Error( + `Cannot route CDP response ${String(message.id)} without its source client.` + ) + } + await this.sendJson(clients[0], message) + return + } + + const serialized = JSON.stringify(message) + const sequence = this.rememberEvent(message.method, serialized) + const clients = this.liveClients().filter( + (connectedClient) => + !message.method.startsWith('Network.') || + (this.networkEnabledClients.has(connectedClient) && + !this.networkReplayingClients.has(connectedClient)) + ) + await Promise.all( + clients.map(async (connectedClient) => { + await this.sendSerialized(connectedClient, serialized) + if (message.method.startsWith('Network.') && sequence !== undefined) { + this.networkReplayCursor.set(connectedClient, sequence) + } }) + ) + } - cdpConnect: if (process.platform !== 'darwin') { - const json = await new Promise<{ webSocketDebuggerUrl: string; id: string }[]>( - (resolve) => { - let count = 0 - let stop = setInterval(async () => { - if (count > 5) { - clearInterval(stop) - resolve([]) - } - try { - count++ - resolve((await fetch(`http://127.0.0.1:${REMOTE_DEBUGGER_PORT}/json`)).json()) - clearInterval(stop) - } catch { - // ignore - } - }, 500) - } - ) - const [data] = json - if (!data) { - break cdpConnect + /** Explicit connection wait primitive; event delivery itself never waits. */ + public waitForClient(): Promise { + const client = this.liveClients()[0] + if (client) return Promise.resolve(client) + if (this.closed) return Promise.reject(new DevtoolServerClosedError()) + return new Promise((resolve, reject) => { + this.clientWaiters.add({ resolve, reject }) + }) + } + + public close(): Promise { + if (this.closePromise) return this.closePromise + this.closed = true + const closedError = new DevtoolServerClosedError() + this.rejectStart?.(closedError) + this.rejectClientWaiters(closedError) + + const webSocketServer = this.webSocketServer + const httpServer = this.httpServer + this.webSocketServer = undefined + this.httpServer = undefined + this._target = undefined + this.eventHistory.splice(0) + this.eventHistoryBytes = 0 + + this.closePromise = Promise.all([ + new Promise((resolve) => { + if (!webSocketServer) { + resolve() + return } - const { id, webSocketDebuggerUrl } = data - const debuggerWs = new WebSocket(webSocketDebuggerUrl) - - debuggerWs.on('open', () => { - const navigateCommand = { - id, - method: 'Page.navigate', - params: { - url - } - } - debuggerWs.send(JSON.stringify(navigateCommand)) - debuggerWs.close() - }) - } + for (const client of webSocketServer.clients) client.terminate() + webSocketServer.close(() => resolve()) + }), + new Promise((resolve) => { + if (!httpServer) { + resolve() + return + } + httpServer.close(() => resolve()) + httpServer.closeAllConnections?.() + }) + ]).then(() => undefined) + + return this.closePromise + } + + private createTarget(address: AddressInfo): DevtoolsTarget { + const authority = `${this.host}:${address.port}` + const webSocketDebuggerUrl = `ws://${authority}${this.webSocketPath}` + const frontendQuery = `${authority}${this.webSocketPath}` + return { + id: this.targetId, + title: this.title, + type: 'node', + url: process.argv[1] ? `file://${process.argv[1]}` : '', + webSocketDebuggerUrl, + devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${frontendQuery}`, + devtoolsFrontendUrlCompat: `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${frontendQuery}`, + discoveryUrl: `http://${authority}/json/list` + } + } - log('opened in chrome or click here to open chrome devtool: ', url) - this.browser = pro + private handleHttpRequest(rawUrl: string | undefined, response: ServerResponse) { + const pathname = this.getPathname(rawUrl) + if (pathname === '/json' || pathname === '/json/list') { + this.writeJson(response, this._target ? [this._target] : []) return - } catch (error) { - console.warn( - "Open devtools failed, but don't worry, you can open it in browser(Chrome or Edge) manually: " + - url + } + if (pathname === '/json/version') { + this.writeJson(response, { + Browser: 'node-network-devtools/2', + 'Protocol-Version': PROTOCOL_VERSION, + 'User-Agent': `Node.js/${process.version}`, + 'V8-Version': process.versions.v8, + webSocketDebuggerUrl: this._target?.webSocketDebuggerUrl + }) + return + } + if (pathname === '/json/protocol') { + this.writeJson(response, LEGACY_CDP_PROTOCOL) + return + } + + response.writeHead(404, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store' + }) + response.end(JSON.stringify({ error: 'Not Found' })) + } + + private writeJson(response: ServerResponse, value: unknown) { + response.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store', + 'Access-Control-Allow-Origin': '*' + }) + response.end(JSON.stringify(value)) + } + + private getPathname(rawUrl: string | undefined): string | undefined { + if (!rawUrl) return undefined + try { + return new URL(rawUrl, `http://${this.host}`).pathname + } catch { + return undefined + } + } + + private rejectUpgrade(socket: Duplex) { + if (socket.writable) { + socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n') + } + socket.destroy() + } + + private attachClient(client: WebSocket) { + const waiters = [...this.clientWaiters] + this.clientWaiters.clear() + waiters.forEach((waiter) => waiter.resolve(client)) + this.onConnect?.() + log('devtool connected') + + client.on('message', (data) => { + void this.handleClientMessage(client, data).catch((error) => this.notifyError(error)) + }) + client.on('close', () => { + this.networkEnabledClients.delete(client) + this.networkReplayingClients.delete(client) + this.networkReplayCursor.delete(client) + log('devtool closed') + this.onClose?.() + }) + client.on('error', (error) => this.notifyError(error)) + } + + private async handleClientMessage(client: WebSocket, data: RawData): Promise { + let parsed: unknown + try { + parsed = JSON.parse(data.toString()) + } catch { + await this.sendProtocolError( + client, + null, + CDP_ERROR_CODES.INVALID_REQUEST, + 'Invalid CDP message: expected JSON object.' ) + return + } + + const candidate = parsed as Partial | null + const id = candidate && this.isCdpId(candidate.id) ? candidate.id : null + if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) { + await this.sendProtocolError( + client, + id, + CDP_ERROR_CODES.INVALID_REQUEST, + 'Invalid CDP request.' + ) + return + } + if (!this.isCdpId(candidate.id) || typeof candidate.method !== 'string' || !candidate.method) { + await this.sendProtocolError( + client, + id, + CDP_ERROR_CODES.INVALID_REQUEST, + 'CDP commands require a number|string id and non-empty method.' + ) + return + } + if ( + candidate.params !== undefined && + (candidate.params === null || + typeof candidate.params !== 'object' || + Array.isArray(candidate.params)) + ) { + await this.sendProtocolError( + client, + candidate.id, + CDP_ERROR_CODES.INVALID_PARAMS, + 'CDP params must be an object.' + ) + return + } + + const request: DevtoolMessageRequest & { id: CdpId } = { + id: candidate.id, + method: candidate.method, + params: candidate.params ?? {} + } + const scope: CommandScope = { client, id: candidate.id, responded: false } + + await this.commandScope.run(scope, async () => { + const context = this.createCommandContext(scope, request) + if (request.method === 'Network.enable') { + await context.result({}) + this.networkEnabledClients.add(client) + this.networkReplayingClients.add(client) + try { + await this.replayNetworkHistory(client) + } finally { + this.networkReplayingClients.delete(client) + } + return + } + if (request.method === 'Network.disable') { + await context.result({}) + this.networkEnabledClients.delete(client) + this.networkReplayingClients.delete(client) + return + } + const builtin = this.builtinCommands.get(request.method) + if (builtin) { + try { + await context.result((await builtin(request.params ?? {})) ?? {}) + } catch (error) { + await context.error( + CDP_ERROR_CODES.INTERNAL_ERROR, + error instanceof Error ? error.message : String(error) + ) + } + return + } + + let handled = false + try { + for (const listener of this.listeners) { + const result = await listener(null, request, context) + if (result === true) handled = true + } + } catch (error) { + if (!scope.responded) { + await context.error( + CDP_ERROR_CODES.INTERNAL_ERROR, + error instanceof Error ? error.message : String(error) + ) + } + return + } + + if (scope.responded) return + if (!handled) { + await context.error(CDP_ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${request.method}`) + return + } + await context.result({}) + }) + } + + private createCommandContext( + scope: CommandScope, + request: DevtoolMessageRequest & { id: CdpId } + ): DevtoolCommandContext { + const reply = async (message: DevtoolMessageResponse | DevtoolErrorResponse) => { + if (scope.responded) return + scope.responded = true + await this.sendJson(scope.client, { ...message, id: scope.id }) + } + return { + client: scope.client, + id: scope.id, + method: request.method, + params: request.params ?? {}, + reply, + result: (result: unknown = {}) => reply({ id: scope.id, result }), + error: (code, message, data) => + reply({ + id: scope.id, + error: { code, message, ...(data === undefined ? {} : { data }) } + }) } } - public close() { - this.server.close() - this.browser && this.browser.kill() + private createBuiltinCommands(): Map< + string, + (params: Record) => BuiltinResult | Promise + > { + const noopMethods = [ + 'Network.setCacheDisabled', + 'Network.setBypassServiceWorker', + 'Network.setExtraHTTPHeaders', + 'Network.setAttachDebugStack', + 'Network.emulateNetworkConditions', + 'Network.emulateNetworkConditionsByRule', + 'Network.overrideNetworkState', + 'Network.setBlockedURLs', + 'Network.clearAcceptedEncodingsOverride', + 'Debugger.disable', + 'Debugger.setPauseOnExceptions', + 'Debugger.setAsyncCallStackDepth', + 'Debugger.setBlackboxPatterns', + 'Runtime.enable', + 'Runtime.disable', + 'Runtime.runIfWaitingForDebugger', + 'Console.enable', + 'Console.disable', + 'Log.enable', + 'Log.disable', + 'Profiler.enable', + 'Profiler.disable', + 'HeapProfiler.enable', + 'HeapProfiler.disable' + ] + const commands = new Map BuiltinResult>( + noopMethods.map((method) => [method, () => ({})]) + ) + commands.set('Network.emulateNetworkConditionsByRule', () => ({ ruleIds: [] })) + commands.set('Debugger.enable', () => ({ debuggerId: this.targetId })) + commands.set('Schema.getDomains', () => ({ + domains: LEGACY_CDP_PROTOCOL.domains.map(({ domain, version }) => ({ name: domain, version })) + })) + return commands + } + + private isResponse( + message: DevtoolMessage + ): message is DevtoolMessageResponse | DevtoolErrorResponse { + return ( + typeof message === 'object' && + message !== null && + 'id' in message && + ('result' in message || 'error' in message) + ) } - async send(message: DevtoolMessage) { - const [socket] = await this.socket - return socket.send(JSON.stringify(message)) + private isCdpId(value: unknown): value is CdpId { + return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value)) + } + + private liveClients(): WebSocket[] { + return [...(this.webSocketServer?.clients ?? [])].filter( + (client) => client.readyState === WebSocket.OPEN + ) + } + + private rejectClientWaiters(error: Error) { + const waiters = [...this.clientWaiters] + this.clientWaiters.clear() + waiters.forEach((waiter) => waiter.reject(error)) + } + + private async sendProtocolError( + client: WebSocket, + id: CdpId | null, + code: number, + message: string, + data?: unknown + ) { + await this.sendJson(client, { + id, + error: { code, message, ...(data === undefined ? {} : { data }) } + }) + } + + private sendJson(client: WebSocket, message: DevtoolMessage): Promise { + return this.sendSerialized(client, JSON.stringify(message)) + } + + private sendSerialized(client: WebSocket, serialized: string): Promise { + if (client.readyState !== WebSocket.OPEN) { + return Promise.reject(new Error('DevTools frontend connection is not open.')) + } + return new Promise((resolve, reject) => { + client.send(serialized, (error) => { + if (error) reject(error) + else resolve() + }) + }) + } + + private rememberEvent(method: string, serialized: string): number | undefined { + const bytes = Buffer.byteLength(serialized) + if (bytes > MAX_BUFFERED_EVENT_BYTES) return undefined + const sequence = ++this.eventSequence + this.eventHistory.push({ + sequence, + method, + serialized, + bytes + }) + this.eventHistoryBytes += bytes + + while ( + this.eventHistory.length > MAX_BUFFERED_EVENTS || + this.eventHistoryBytes > MAX_BUFFERED_EVENT_BYTES + ) { + const removed = this.eventHistory.shift() + if (removed) this.eventHistoryBytes -= removed.bytes + } + return sequence + } + + private async replayNetworkHistory(client: WebSocket) { + while (client.readyState === WebSocket.OPEN) { + const cursor = this.networkReplayCursor.get(client) ?? 0 + const snapshot = this.eventHistory.filter( + (event) => event.sequence > cursor && event.method.startsWith('Network.') + ) + if (snapshot.length === 0) return + for (const event of snapshot) { + await this.sendSerialized(client, event.serialized) + this.networkReplayCursor.set(client, event.sequence) + } + } + } + + private notifyError(error: unknown) { + for (const listener of this.listeners) { + void Promise.resolve(listener(error)).catch(() => undefined) + } } } + +const LEGACY_CDP_PROTOCOL = Object.freeze({ + version: { major: '1', minor: '3' }, + domains: [ + { + domain: 'Network', + version: '1.3', + commands: [ + { name: 'enable' }, + { name: 'disable' }, + { name: 'setAttachDebugStack' }, + { + name: 'emulateNetworkConditionsByRule', + returns: [{ name: 'ruleIds', type: 'array', items: { type: 'string' } }] + }, + { name: 'overrideNetworkState' }, + { name: 'clearAcceptedEncodingsOverride' }, + { + name: 'getResponseBody', + parameters: [{ name: 'requestId', type: 'string' }], + returns: [ + { name: 'body', type: 'string' }, + { name: 'base64Encoded', type: 'boolean' } + ] + } + ], + events: [ + { name: 'requestWillBeSent' }, + { name: 'responseReceived' }, + { name: 'dataReceived' }, + { name: 'loadingFinished' }, + { name: 'loadingFailed' }, + { name: 'eventSourceMessageReceived' }, + { name: 'webSocketCreated' }, + { name: 'webSocketWillSendHandshakeRequest' }, + { name: 'webSocketHandshakeResponseReceived' }, + { name: 'webSocketFrameSent' }, + { name: 'webSocketFrameReceived' }, + { name: 'webSocketClosed' } + ] + }, + { + domain: 'Debugger', + version: '1.3', + commands: [ + { name: 'enable' }, + { name: 'disable' }, + { + name: 'getScriptSource', + parameters: [{ name: 'scriptId', type: 'string' }], + returns: [{ name: 'scriptSource', type: 'string' }] + } + ], + events: [{ name: 'scriptParsed' }] + }, + { domain: 'Runtime', version: '1.3', commands: [{ name: 'enable' }, { name: 'disable' }] }, + { domain: 'Schema', version: '1.3', commands: [{ name: 'getDomains' }] } + ] +}) diff --git a/packages/network-debugger/src/fork/devtool/type.ts b/packages/network-debugger/src/fork/devtool/type.ts index c39c2a4..f55257d 100644 --- a/packages/network-debugger/src/fork/devtool/type.ts +++ b/packages/network-debugger/src/fork/devtool/type.ts @@ -1,33 +1,69 @@ +import type { WebSocket } from 'ws' +import type { CdpId } from '../../legacy-bridge/contracts' + export interface DevtoolMessageRequest { + id?: CdpId method: string - params: Record + params?: Record } export interface DevtoolMessageResponse { - id: string - result: any + id: CdpId + result: unknown + /** @deprecated CDP responses are correlated by `id`, not a method name. */ method?: string } export interface DevtoolErrorResponse { - id: string - error: { code: number; message?: string } + id: CdpId | null + error: { + code: number + message?: string + data?: unknown + } } export type DevtoolMessage = DevtoolMessageRequest | DevtoolMessageResponse | DevtoolErrorResponse +export interface DevtoolCommandContext { + /** The exact frontend connection that issued this command. */ + client: WebSocket + id: CdpId + method: string + params: Record + /** Send a complete response to the issuing connection. First response wins. */ + reply(message: DevtoolMessageResponse | DevtoolErrorResponse): Promise + /** Resolve the command for the issuing connection. */ + result(result?: unknown): Promise + /** Reject the command for the issuing connection. */ + error(code: number, message: string, data?: unknown): Promise +} + +export type DevtoolProtocolListener = ( + error: unknown | null, + message?: DevtoolMessage, + context?: DevtoolCommandContext +) => boolean | void | Promise + export class BaseDevtoolServer { public timestamp = 0 - private startTime = Date.now() + private readonly startTime = Date.now() + public readonly listeners: DevtoolProtocolListener[] = [] + public getTimestamp() { this.updateTimestamp() return this.timestamp } - public listeners: ((error: unknown | null, message?: any) => void)[] = [] + public updateTimestamp() { this.timestamp = (Date.now() - this.startTime) / 1000 } - public on(listener: (error: unknown | null, message?: any) => void) { + + public on(listener: DevtoolProtocolListener) { this.listeners.push(listener) + return () => { + const index = this.listeners.indexOf(listener) + if (index >= 0) this.listeners.splice(index, 1) + } } } diff --git a/packages/network-debugger/src/fork/fork.ts b/packages/network-debugger/src/fork/fork.ts index 46be791..2f31456 100644 --- a/packages/network-debugger/src/fork/fork.ts +++ b/packages/network-debugger/src/fork/fork.ts @@ -1,57 +1,9 @@ -import { RequestCenter } from './request-center' -import { PORT, RegisterOptions, SERVER_PORT } from '../common' -import { loadPlugin } from './module' -import { jsonParse } from '../utils' - -const options = jsonParse(process.env.NETWORK_OPTIONS || '{}', {}) - -const loadCenter = () => { - const main = new RequestCenter({ - serverPort: options.serverPort || SERVER_PORT, - port: options.port || PORT, - autoOpenDevtool: options.autoOpenDevtool - }) - loadPlugin(main) - return main -} - -let main = loadCenter() - -let restartCount = 0 -const restartLimit = 5 -const cleanRestartCountInterval = 30 * 1000 -const restart = () => { - setTimeout( - () => { - restartCount++ - if (restartCount >= restartLimit) { - console.error('Restart limit reached') - clean() - return - } - main.close() - main = loadCenter() - }, - 10 + Math.random() * 100 - ) -} - -setInterval(() => { - restartCount = 0 -}, cleanRestartCountInterval) - -const clean = () => { - process.exit(0) -} -process.on('exit', clean) -process.on('SIGINT', clean) -process.on('SIGTERM', clean) -process.on('beforeExit', clean) -process.on('uncaughtException', (e) => { - console.error('uncaughtException: ', e) - restart() -}) -process.on('unhandledRejection', (e) => { - console.error('unhandledRejection: ', e) - restart() +import { runLegacyBridgeHost } from '../legacy-bridge/host' + +void runLegacyBridgeHost().catch((error) => { + // The host already sends a structured startup diagnostic when IPC is + // available. This remains useful when someone launches the child directly. + console.error('Legacy bridge child failed:', error) + process.exitCode = 1 + if (process.connected) process.disconnect() }) diff --git a/packages/network-debugger/src/fork/module/common.ts b/packages/network-debugger/src/fork/module/common.ts index 9098f1d..74a2974 100644 --- a/packages/network-debugger/src/fork/module/common.ts +++ b/packages/network-debugger/src/fork/module/common.ts @@ -1,12 +1,24 @@ -import { DevtoolServer } from '../devtool' -import { DevtoolMessageListener, RequestCenter } from '../request-center' +import type { DevtoolMessage } from '../devtool' +import type { DevtoolMessageListener } from '../request-center' + +export interface PluginDevtool { + timestamp: number + getTimestamp(): number + updateTimestamp(): void + send(message: DevtoolMessage): Promise +} + +export interface PluginCore { + on(method: string, listener: DevtoolMessageListener): unknown + usePlugin(id: string): T +} export interface PluginContext { - devtool: DevtoolServer - core: RequestCenter + devtool: PluginDevtool + core: PluginCore } let currentPluginContext: PluginContext | null = null -const initPluginContext = (core: RequestCenter, devtool: DevtoolServer) => { +const initPluginContext = (core: PluginCore, devtool: PluginDevtool) => { currentPluginContext = { devtool, core @@ -15,8 +27,8 @@ const initPluginContext = (core: RequestCenter, devtool: DevtoolServer) => { const resetPluginContext = () => (currentPluginContext = null) export interface CoreCotext { - devtool: DevtoolServer - core: RequestCenter + devtool: PluginDevtool + core: PluginCore plugins: PluginInstance[] } diff --git a/packages/network-debugger/src/fork/module/debugger/index.test.ts b/packages/network-debugger/src/fork/module/debugger/index.test.ts index ba45d89..1469a0b 100644 --- a/packages/network-debugger/src/fork/module/debugger/index.test.ts +++ b/packages/network-debugger/src/fork/module/debugger/index.test.ts @@ -1,332 +1,142 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { CDP_ERROR_CODES } from '../../devtool' import type { DevtoolMessageListener } from '../../request-center' - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { - mockCoreOn, - mockDevtoolSend, - registeredHandlers, - mockUsePlugin, - mockGetScriptSource, - mockGetLocalScriptList -} = vi.hoisted(() => { - const handlers = new Map[]>() - return { - mockCoreOn: vi.fn((type: string, fn: DevtoolMessageListener) => { - if (!handlers.has(type)) { - handlers.set(type, []) - } - handlers.get(type)!.push(fn) - return () => { - const list = handlers.get(type) - if (list) { - const index = list.indexOf(fn) - if (index > -1) { - list.splice(index, 1) - } - } - } - }), - mockDevtoolSend: vi.fn(), - registeredHandlers: handlers, - mockUsePlugin: vi.fn(), - mockGetScriptSource: vi.fn(), - mockGetLocalScriptList: vi.fn() +import type { PluginCore } from '../common' +import { debuggerPlugin } from './index' + +const handlers = new Map[]>() +const send = vi.fn().mockResolvedValue(undefined) +const getScriptSource = vi.fn() +const getLocalScriptList = vi.fn() + +function loadPlugin() { + const networkPlugin = { + getRequest: vi.fn(), + resourceService: { + getScriptSource, + getLocalScriptList, + getScriptIdByUrl: vi.fn() + } } -}) - -// Mock DevtoolServer 和 RequestCenter 模块 -vi.mock('../../devtool', () => ({ - DevtoolServer: vi.fn() -})) - -vi.mock('../../request-center', () => ({ - RequestCenter: vi.fn() -})) - -// 创建测试用的 mock 上下文对象 -const createMockContext = () => ({ - devtool: { - send: mockDevtoolSend, - getTimestamp: vi.fn().mockReturnValue(0), - updateTimestamp: vi.fn(), - timestamp: 0 - }, - core: { - on: mockCoreOn, - usePlugin: mockUsePlugin - }, - plugins: [] -}) - -describe('fork/module/debugger/index.ts', () => { - beforeEach(() => { - vi.clearAllMocks() - registeredHandlers.clear() - - // 设置 mockUsePlugin 返回 network 插件的 mock - mockUsePlugin.mockReturnValue({ - getRequest: vi.fn(), - resourceService: { - getScriptSource: mockGetScriptSource, - getLocalScriptList: mockGetLocalScriptList, - getScriptIdByUrl: vi.fn() - } - }) - - // 默认返回空脚本列表 - mockGetLocalScriptList.mockReturnValue([]) + const core: PluginCore = { + on(method: string, listener: DevtoolMessageListener) { + const list = handlers.get(method) ?? [] + list.push(listener as DevtoolMessageListener) + handlers.set(method, list) + return () => undefined + }, + usePlugin() { + return networkPlugin as unknown as T + } + } + debuggerPlugin({ + devtool: { + send, + timestamp: 0, + getTimestamp: () => 0, + updateTimestamp: () => undefined + }, + core, + plugins: [debuggerPlugin] }) +} + +function handler(method: string) { + const listener = handlers.get(method)?.[0] + if (!listener) throw new Error(`Missing handler ${method}`) + return listener +} + +beforeEach(() => { + vi.clearAllMocks() + handlers.clear() + getLocalScriptList.mockReturnValue([]) + loadPlugin() +}) - afterEach(() => { - vi.restoreAllMocks() +describe('debuggerPlugin v2 command semantics', () => { + test('registers lazy source and reconnect handlers', () => { + expect(debuggerPlugin.id).toBe('debugger') + expect(handlers.has('Debugger.getScriptSource')).toBe(true) + expect(handlers.has('onConnect')).toBe(true) }) - describe('debuggerPlugin', () => { - test('插件具有正确的 id', async () => { - const { debuggerPlugin } = await import('./index') - - expect(debuggerPlugin.id).toBe('debugger') - }) - - test('插件使用 network 插件', async () => { - const { debuggerPlugin } = await import('./index') + test('resolves getScriptSource through the command result callback', async () => { + getScriptSource.mockReturnValue('console.log("source")') + const result = vi.fn().mockResolvedValue(undefined) + const error = vi.fn().mockResolvedValue(undefined) - // 直接调用插件函数,使用 Function.prototype.call 绕过类型检查 - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - expect(mockUsePlugin).toHaveBeenCalledWith('network') + await handler('Debugger.getScriptSource')({ + data: { scriptId: 'script-1' }, + id: 0, + result, + error }) - test('插件注册 Debugger.getScriptSource 处理器', async () => { - const { debuggerPlugin } = await import('./index') - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - expect(mockCoreOn).toHaveBeenCalledWith('Debugger.getScriptSource', expect.any(Function)) - }) - - test('插件注册 onConnect 处理器', async () => { - const { debuggerPlugin } = await import('./index') - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - expect(mockCoreOn).toHaveBeenCalledWith('onConnect', expect.any(Function)) - }) + expect(getScriptSource).toHaveBeenCalledWith('script-1') + expect(result).toHaveBeenCalledWith({ scriptSource: 'console.log("source")' }) + expect(error).not.toHaveBeenCalled() + expect(send).not.toHaveBeenCalledWith(expect.objectContaining({ id: 0 })) }) - describe('Debugger.getScriptSource 处理器', () => { - test('返回脚本源代码', async () => { - const { debuggerPlugin } = await import('./index') - - mockGetScriptSource.mockReturnValue('console.log("hello world");') - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - const getScriptSourceHandlers = registeredHandlers.get('Debugger.getScriptSource') - expect(getScriptSourceHandlers).toBeDefined() - - getScriptSourceHandlers![0]({ - data: { scriptId: 'script-123' }, - id: 'request-id-1' - }) - - expect(mockGetScriptSource).toHaveBeenCalledWith('script-123') - expect(mockDevtoolSend).toHaveBeenCalledWith({ - id: 'request-id-1', - method: 'Debugger.getScriptSourceResponse', - result: { - scriptSource: 'console.log("hello world");' - } - }) - }) - - test('脚本不存在时返回 null', async () => { - const { debuggerPlugin } = await import('./index') - - mockGetScriptSource.mockReturnValue(null) - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - const getScriptSourceHandlers = registeredHandlers.get('Debugger.getScriptSource') - getScriptSourceHandlers![0]({ - data: { scriptId: 'non-existent-script' }, - id: 'request-id-2' - }) + test('rejects invalid params with -32602', async () => { + const result = vi.fn().mockResolvedValue(undefined) + const error = vi.fn().mockResolvedValue(undefined) - expect(mockDevtoolSend).toHaveBeenCalledWith({ - id: 'request-id-2', - method: 'Debugger.getScriptSourceResponse', - result: { - scriptSource: null - } - }) - }) - - test('没有 id 时不发送响应', async () => { - const { debuggerPlugin } = await import('./index') - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) + await handler('Debugger.getScriptSource')({ data: {}, result, error }) - const getScriptSourceHandlers = registeredHandlers.get('Debugger.getScriptSource') - getScriptSourceHandlers![0]({ - data: { scriptId: 'script-123' }, - id: undefined - }) - - expect(mockDevtoolSend).not.toHaveBeenCalled() - }) + expect(error).toHaveBeenCalledWith(CDP_ERROR_CODES.INVALID_PARAMS, 'scriptId must be a string.') + expect(result).not.toHaveBeenCalled() }) - describe('onConnect 处理器', () => { - test('连接时发送所有已解析的脚本', async () => { - const { debuggerPlugin } = await import('./index') - - const scriptList = [ - { - url: 'file:///path/to/script1.js', - scriptLanguage: 'JavaScript', - embedderName: 'file:///path/to/script1.js', - scriptId: '1', - sourceMapURL: '', - hasSourceURL: false - }, - { - url: 'file:///path/to/script2.js', - scriptLanguage: 'JavaScript', - embedderName: 'file:///path/to/script2.js', - scriptId: '2', - sourceMapURL: 'file:///path/to/script2.js.map', - hasSourceURL: true - } - ] - - mockGetLocalScriptList.mockReturnValue(scriptList) - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - // 触发 onConnect - const onConnectHandlers = registeredHandlers.get('onConnect') - expect(onConnectHandlers).toBeDefined() - onConnectHandlers![0]({ data: null, id: undefined }) + test('rejects an unknown lazy script with a server error', async () => { + getScriptSource.mockReturnValue(null) + const result = vi.fn().mockResolvedValue(undefined) + const error = vi.fn().mockResolvedValue(undefined) - // 验证发送了所有脚本 - expect(mockDevtoolSend).toHaveBeenCalledWith({ - method: 'Debugger.scriptParsed', - params: scriptList[0] - }) - expect(mockDevtoolSend).toHaveBeenCalledWith({ - method: 'Debugger.scriptParsed', - params: scriptList[1] - }) + await handler('Debugger.getScriptSource')({ + data: { scriptId: 'missing' }, + result, + error }) - test('没有脚本时不发送消息', async () => { - const { debuggerPlugin } = await import('./index') - - mockGetLocalScriptList.mockReturnValue([]) - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - // 触发 onConnect - const onConnectHandlers = registeredHandlers.get('onConnect') - onConnectHandlers![0]({ data: null, id: undefined }) - - // 不应该发送任何消息 - expect(mockDevtoolSend).not.toHaveBeenCalled() - }) - - test('发送包含 sourceMapURL 的脚本信息', async () => { - const { debuggerPlugin } = await import('./index') - - const scriptWithSourceMap = { - url: 'file:///path/to/app.js', - scriptLanguage: 'JavaScript', - embedderName: 'file:///path/to/app.js', - scriptId: '100', - sourceMapURL: 'data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==', - hasSourceURL: true - } - - mockGetLocalScriptList.mockReturnValue([scriptWithSourceMap]) - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - const onConnectHandlers = registeredHandlers.get('onConnect') - onConnectHandlers![0]({ data: null, id: undefined }) - - expect(mockDevtoolSend).toHaveBeenCalledWith({ - method: 'Debugger.scriptParsed', - params: expect.objectContaining({ - sourceMapURL: 'data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==', - hasSourceURL: true - }) - }) - }) + expect(error).toHaveBeenCalledWith(CDP_ERROR_CODES.SERVER_ERROR, 'Unknown script id missing.') + expect(result).not.toHaveBeenCalled() }) - describe('ScriptSourceData 接口', () => { - test('处理器接收正确的数据格式', async () => { - const { debuggerPlugin } = await import('./index') - - mockGetScriptSource.mockReturnValue('// script content') - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - const getScriptSourceHandlers = registeredHandlers.get('Debugger.getScriptSource') - - // 验证处理器可以正确处理 ScriptSourceData 格式的数据 - getScriptSourceHandlers![0]({ - data: { scriptId: 'test-script-id' }, - id: 'test-request-id' - }) - - expect(mockGetScriptSource).toHaveBeenCalledWith('test-script-id') + test('reads the current lazy script list on every frontend connection', () => { + const first = { + url: 'file:///first.js', + scriptLanguage: 'JavaScript', + embedderName: 'file:///first.js', + scriptId: '1', + sourceMapURL: '', + hasSourceURL: false + } + const second = { + ...first, + url: 'file:///second.js', + embedderName: 'file:///second.js', + scriptId: '2' + } + getLocalScriptList.mockReturnValueOnce([first]).mockReturnValueOnce([first, second]) + + handler('onConnect')({ data: null }) + handler('onConnect')({ data: null }) + + expect(getLocalScriptList).toHaveBeenCalledTimes(2) + expect(send).toHaveBeenNthCalledWith(1, { + method: 'Debugger.scriptParsed', + params: first + }) + expect(send).toHaveBeenNthCalledWith(3, { + method: 'Debugger.scriptParsed', + params: second }) }) - describe('IScriptParsed 接口', () => { - test('脚本信息包含所有必要字段', async () => { - const { debuggerPlugin } = await import('./index') - - const completeScript = { - url: 'file:///complete/script.js', - scriptLanguage: 'JavaScript', - embedderName: 'file:///complete/script.js', - scriptId: '999', - sourceMapURL: 'file:///complete/script.js.map', - hasSourceURL: true - } - - mockGetLocalScriptList.mockReturnValue([completeScript]) - - const context = createMockContext() - ;(debuggerPlugin as Function)(context) - - const onConnectHandlers = registeredHandlers.get('onConnect') - onConnectHandlers![0]({ data: null, id: undefined }) - - expect(mockDevtoolSend).toHaveBeenCalledWith({ - method: 'Debugger.scriptParsed', - params: { - url: 'file:///complete/script.js', - scriptLanguage: 'JavaScript', - embedderName: 'file:///complete/script.js', - scriptId: '999', - sourceMapURL: 'file:///complete/script.js.map', - hasSourceURL: true - } - }) - }) + test('does not announce scripts before lazy registration', () => { + handler('onConnect')({ data: null }) + expect(send).not.toHaveBeenCalled() }) }) diff --git a/packages/network-debugger/src/fork/module/debugger/index.ts b/packages/network-debugger/src/fork/module/debugger/index.ts index 3196460..25b780e 100644 --- a/packages/network-debugger/src/fork/module/debugger/index.ts +++ b/packages/network-debugger/src/fork/module/debugger/index.ts @@ -1,3 +1,4 @@ +import { CDP_ERROR_CODES } from '../../devtool' import { createPlugin, useConnect, useHandler } from '../common' import { NetworkPluginCore } from '../network' export interface ISciprtParsed { @@ -15,27 +16,25 @@ export interface ScriptSourceData { export const debuggerPlugin = createPlugin('debugger', ({ devtool, core }) => { const networkPlugin = core.usePlugin('network') - useHandler('Debugger.getScriptSource', ({ id, data }) => { - if (!id) { + useHandler('Debugger.getScriptSource', async ({ data, result, error }) => { + if (!data || typeof data.scriptId !== 'string') { + await error?.(CDP_ERROR_CODES.INVALID_PARAMS, 'scriptId must be a string.') return } const { scriptId } = data const scriptSource = networkPlugin.resourceService.getScriptSource(scriptId) - devtool.send({ - id: id, - method: 'Debugger.getScriptSourceResponse', - result: { - scriptSource - } - }) + if (typeof scriptSource !== 'string') { + await error?.(CDP_ERROR_CODES.SERVER_ERROR, `Unknown script id ${scriptId}.`) + return + } + await result?.({ scriptSource }) }) - const scriptList = networkPlugin.resourceService.getLocalScriptList() useConnect(() => { - scriptList.forEach((script) => { + networkPlugin.resourceService.getLocalScriptList().forEach((script) => { devtool.send({ method: 'Debugger.scriptParsed', - params: script + params: { ...script } }) }) }) diff --git a/packages/network-debugger/src/fork/module/health/index.test.ts b/packages/network-debugger/src/fork/module/health/index.test.ts deleted file mode 100644 index 2f1efc0..0000000 --- a/packages/network-debugger/src/fork/module/health/index.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import type { DevtoolMessageListener } from '../../request-center' - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { mockCoreOn, mockDevtoolSend, registeredHandlers } = vi.hoisted(() => { - const handlers = new Map[]>() - return { - mockCoreOn: vi.fn((type: string, fn: DevtoolMessageListener) => { - if (!handlers.has(type)) { - handlers.set(type, []) - } - handlers.get(type)!.push(fn) - return () => { - const list = handlers.get(type) - if (list) { - const index = list.indexOf(fn) - if (index > -1) { - list.splice(index, 1) - } - } - } - }), - mockDevtoolSend: vi.fn(), - registeredHandlers: handlers - } -}) - -// 定义 mock 对象的接口类型 -interface MockDevtoolServer { - send: ReturnType - close: ReturnType - open: ReturnType - on: ReturnType - listeners: Array<() => void> - timestamp: number - getTimestamp: ReturnType - updateTimestamp: ReturnType -} - -interface MockRequestCenter { - on: typeof mockCoreOn - loadPlugins: ReturnType - usePlugin: ReturnType - close: ReturnType -} - -// 创建 mock 对象 -function createMockDevtool(): MockDevtoolServer { - return { - send: mockDevtoolSend, - close: vi.fn(), - open: vi.fn(), - on: vi.fn(), - listeners: [], - timestamp: 0, - getTimestamp: vi.fn().mockReturnValue(0), - updateTimestamp: vi.fn() - } -} - -function createMockCore(): MockRequestCenter { - return { - on: mockCoreOn, - loadPlugins: vi.fn(), - usePlugin: vi.fn(), - close: vi.fn() - } -} - -describe('fork/module/health/index.ts', () => { - let originalProcessExit: typeof process.exit - - beforeEach(() => { - vi.clearAllMocks() - registeredHandlers.clear() - vi.useFakeTimers() - // 保存原始的 process.exit - originalProcessExit = process.exit - // Mock process.exit - process.exit = vi.fn() as never - }) - - afterEach(() => { - vi.restoreAllMocks() - vi.useRealTimers() - // 恢复原始的 process.exit - process.exit = originalProcessExit - }) - - describe('healthPlugin', () => { - test('插件具有正确的 id', async () => { - const { healthPlugin } = await import('./index') - - expect(healthPlugin.id).toBe('health') - }) - - test('插件注册 healthcheck 处理器', async () => { - const { healthPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - healthPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [healthPlugin] - }) - - expect(mockCoreOn).toHaveBeenCalledWith('healthcheck', expect.any(Function)) - }) - - test('10 秒内没有 healthcheck 时退出进程', async () => { - const { healthPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - healthPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [healthPlugin] - }) - - // 快进 10 秒 - vi.advanceTimersByTime(10000) - - expect(process.exit).toHaveBeenCalledWith(0) - }) - - test('收到 healthcheck 后重置超时计时器', async () => { - const { healthPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - healthPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [healthPlugin] - }) - - // 快进 5 秒 - vi.advanceTimersByTime(5000) - - // 触发 healthcheck - const healthcheckHandlers = registeredHandlers.get('healthcheck') - expect(healthcheckHandlers).toBeDefined() - expect(healthcheckHandlers!.length).toBeGreaterThan(0) - healthcheckHandlers![0]({ data: null, id: undefined }) - - // 再快进 5 秒(总共 10 秒,但因为重置了计时器,不应该退出) - vi.advanceTimersByTime(5000) - - expect(process.exit).not.toHaveBeenCalled() - - // 再快进 5 秒(从上次 healthcheck 开始已经 10 秒) - vi.advanceTimersByTime(5000) - - expect(process.exit).toHaveBeenCalledWith(0) - }) - - test('多次 healthcheck 持续重置计时器', async () => { - const { healthPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - healthPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [healthPlugin] - }) - - const healthcheckHandlers = registeredHandlers.get('healthcheck') - expect(healthcheckHandlers).toBeDefined() - - // 模拟多次 healthcheck - for (let i = 0; i < 5; i++) { - vi.advanceTimersByTime(5000) - healthcheckHandlers![0]({ data: null, id: undefined }) - } - - // 总共过了 25 秒,但每次都重置了计时器,所以不应该退出 - expect(process.exit).not.toHaveBeenCalled() - - // 再等 10 秒不发送 healthcheck - vi.advanceTimersByTime(10000) - - expect(process.exit).toHaveBeenCalledWith(0) - }) - - test('healthcheck 处理器接收正确的参数格式', async () => { - const { healthPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - healthPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [healthPlugin] - }) - - const healthcheckHandlers = registeredHandlers.get('healthcheck') - expect(healthcheckHandlers).toBeDefined() - - // 验证处理器可以正常调用 - expect(() => { - healthcheckHandlers![0]({ data: { timestamp: Date.now() }, id: '123' }) - }).not.toThrow() - }) - }) -}) diff --git a/packages/network-debugger/src/fork/module/health/index.ts b/packages/network-debugger/src/fork/module/health/index.ts deleted file mode 100644 index c37b179..0000000 --- a/packages/network-debugger/src/fork/module/health/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createPlugin, useHandler } from '../common' - -export const healthPlugin = createPlugin('health', ({ devtool }) => { - const exitProcess = () => { - process.exit(0) - } - - let id = setTimeout(exitProcess, 10000) - useHandler('healthcheck', () => { - clearTimeout(id) - id = setTimeout(exitProcess, 10000) - }) -}) diff --git a/packages/network-debugger/src/fork/module/index.ts b/packages/network-debugger/src/fork/module/index.ts index bce3cd8..30a6507 100644 --- a/packages/network-debugger/src/fork/module/index.ts +++ b/packages/network-debugger/src/fork/module/index.ts @@ -2,8 +2,7 @@ import { RequestCenter } from '../request-center' import { debuggerPlugin } from './debugger' import { networkPlugin } from './network' import { websocketPlugin } from './websocket' -import { healthPlugin } from './health' export const loadPlugin = (instance: RequestCenter) => { - instance.loadPlugins([healthPlugin, networkPlugin, debuggerPlugin, websocketPlugin]) + instance.loadPlugins([networkPlugin, debuggerPlugin, websocketPlugin]) } diff --git a/packages/network-debugger/src/fork/module/network/index.test.ts b/packages/network-debugger/src/fork/module/network/index.test.ts index 8ca9a61..f5c94e8 100644 --- a/packages/network-debugger/src/fork/module/network/index.test.ts +++ b/packages/network-debugger/src/fork/module/network/index.test.ts @@ -1,1167 +1,373 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import zlib from 'node:zlib' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { RequestDetail } from '../../../common' +import { CDP_ERROR_CODES } from '../../devtool' import type { DevtoolMessageListener } from '../../request-center' - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { mockCoreOn, mockDevtoolSend, registeredHandlers, mockUsePlugin } = vi.hoisted(() => { - const handlers = new Map[]>() - return { - mockCoreOn: vi.fn((type: string, fn: DevtoolMessageListener) => { - if (!handlers.has(type)) { - handlers.set(type, []) - } - handlers.get(type)!.push(fn) - return () => { - const list = handlers.get(type) - if (list) { - const index = list.indexOf(fn) - if (index > -1) { - list.splice(index, 1) - } - } - } - }), - mockDevtoolSend: vi.fn(), - registeredHandlers: handlers, - mockUsePlugin: vi.fn() - } -}) - -// 定义 mock 对象的接口类型 -interface MockDevtoolServer { - send: ReturnType - close: ReturnType - open: ReturnType - on: ReturnType - listeners: Array<() => void> - timestamp: number - getTimestamp: ReturnType - updateTimestamp: ReturnType +import { networkPlugin, toMimeType, type NetworkPluginCore } from './index' + +const handlers = new Map[]>() +const send = vi.fn().mockResolvedValue(undefined) +let timestamp = 100 +let plugin: NetworkPluginCore +let fixtureDir = '' + +function request(overrides: Partial = {}) { + const detail = new RequestDetail() + detail.url = 'http://127.0.0.1:43123/api?source=dynamic' + detail.method = 'GET' + detail.requestHeaders = {} + detail.responseHeaders = {} + detail.responseInfo = {} + detail.requestStartTime = 1_787_891_800.25 + Object.assign(detail, overrides) + return detail } -interface MockRequestCenter { - on: typeof mockCoreOn - loadPlugins: ReturnType - usePlugin: typeof mockUsePlugin - close: ReturnType -} - -// 创建 mock 对象 -function createMockDevtool(): MockDevtoolServer { - return { - send: mockDevtoolSend, - close: vi.fn(), - open: vi.fn(), - on: vi.fn(), - listeners: [], - timestamp: 0, - getTimestamp: vi.fn().mockReturnValue(0), - updateTimestamp: vi.fn() +function loadPlugin() { + const core = { + on(method: string, listener: DevtoolMessageListener) { + const list = handlers.get(method) ?? [] + list.push(listener) + handlers.set(method, list) + return () => undefined + }, + usePlugin: vi.fn() } + return networkPlugin({ + devtool: { + send, + timestamp: 0, + getTimestamp: () => (timestamp += 0.25), + updateTimestamp: () => undefined + }, + core, + plugins: [networkPlugin] + }) } -function createMockCore(): MockRequestCenter { - return { - on: mockCoreOn, - loadPlugins: vi.fn(), - usePlugin: mockUsePlugin, - close: vi.fn() - } +function handler(method: string) { + const listener = handlers.get(method)?.[0] + if (!listener) throw new Error(`Missing handler ${method}`) + return listener } -// 创建测试用的 RequestDetail -function createTestRequest(overrides: Partial = {}): RequestDetail { - const request = new RequestDetail() - request.id = overrides.id || 'test-request-id' - request.url = overrides.url || 'http://example.com/api/test' - request.method = overrides.method || 'GET' - request.requestHeaders = overrides.requestHeaders || { 'Content-Type': 'application/json' } - request.requestData = overrides.requestData - request.responseData = overrides.responseData - request.responseStatusCode = overrides.responseStatusCode || 200 - request.responseHeaders = overrides.responseHeaders || { 'Content-Type': 'application/json' } - request.responseInfo = overrides.responseInfo || { encodedDataLength: 100, dataLength: 100 } - request.requestStartTime = overrides.requestStartTime || Date.now() - request.requestEndTime = overrides.requestEndTime - request.initiator = overrides.initiator - return request +function messages() { + return send.mock.calls.map(([message]) => message as Record) } -describe('fork/module/network/index.ts', () => { - beforeEach(() => { - vi.clearAllMocks() - registeredHandlers.clear() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('toMimeType 函数', () => { - test('从 content-type 中提取 mime type', async () => { - const { toMimeType } = await import('./index') - - expect(toMimeType('application/json; charset=utf-8')).toBe('application/json') - expect(toMimeType('text/html; charset=utf-8')).toBe('text/html') - expect(toMimeType('image/png')).toBe('image/png') - }) - - test('没有分号时返回完整的 content-type', async () => { - const { toMimeType } = await import('./index') - - expect(toMimeType('application/json')).toBe('application/json') - expect(toMimeType('text/plain')).toBe('text/plain') - }) - - test('空字符串返回 text/plain', async () => { - const { toMimeType } = await import('./index') - - expect(toMimeType('')).toBe('text/plain') - }) - }) - - describe('networkPlugin', () => { - test('插件具有正确的 id', async () => { - const { networkPlugin } = await import('./index') - - expect(networkPlugin.id).toBe('network') - }) - - test('插件注册所有必要的处理器', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 验证注册了所有必要的处理器 - expect(mockCoreOn).toHaveBeenCalledWith('Network.getResponseBody', expect.any(Function)) - expect(mockCoreOn).toHaveBeenCalledWith('initRequest', expect.any(Function)) - expect(mockCoreOn).toHaveBeenCalledWith('registerRequest', expect.any(Function)) - expect(mockCoreOn).toHaveBeenCalledWith('endRequest', expect.any(Function)) - expect(mockCoreOn).toHaveBeenCalledWith('responseData', expect.any(Function)) - }) - - test('插件返回 getRequest 和 resourceService', async () => { - const { networkPlugin } = await import('./index') +function methods() { + return messages().map((message) => message.method) +} - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() +beforeEach(() => { + vi.clearAllMocks() + handlers.clear() + timestamp = 100 + fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nnd-network-plugin-')) + plugin = loadPlugin() +}) - const result = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) +afterEach(() => { + fs.rmSync(fixtureDir, { recursive: true, force: true }) +}) - expect(result).toHaveProperty('getRequest') - expect(result).toHaveProperty('resourceService') - expect(typeof result.getRequest).toBe('function') - }) +describe('networkPlugin v2 lifecycle', () => { + test('registers capture and asynchronous CDP command handlers', () => { + expect(networkPlugin.id).toBe('network') + expect([...handlers.keys()]).toEqual( + expect.arrayContaining([ + 'initRequest', + 'registerRequest', + 'updateRequest', + 'responseReceived', + 'responseData', + 'requestFailed', + 'endRequest', + 'eventSourceResponseReceived', + 'eventSourceMessage', + 'Network.getResponseBody', + 'Network.getRequestPostData' + ]) + ) + expect(plugin.requestCount()).toBe(0) }) - describe('initRequest 处理器', () => { - test('初始化请求并存储', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - const result = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ id: 'init-test-id' }) - - // 触发 initRequest 处理器 - const initRequestHandlers = registeredHandlers.get('initRequest') - expect(initRequestHandlers).toBeDefined() - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 验证请求被存储 - const storedRequest = result.getRequest('init-test-id') - expect(storedRequest).toBeDefined() - expect(storedRequest.id).toBe('init-test-id') - }) - - test('处理带有 initiator 的请求', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - const result = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ - id: 'initiator-test-id', - initiator: { - type: 'script', - stack: { - callFrames: [ - { - columnNumber: 10, - functionName: 'testFunction', - lineNumber: 20, - url: '/path/to/file.js' - } - ] - } + test('preserves the real dynamic-origin URL and emits second-based timestamps', async () => { + const detail = request({ + method: 'POST', + requestHeaders: { 'content-type': 'application/json' }, + requestData: Buffer.from('{"ok":true}'), + // Accept old millisecond inputs at the bridge boundary. + requestStartTime: 1_787_891_800_250 + }) + + await handler('registerRequest')({ data: detail }) + + const event = messages()[0] + expect(event).toMatchObject({ + method: 'Network.requestWillBeSent', + params: { + documentURL: 'http://127.0.0.1:43123/api?source=dynamic', + wallTime: 1_787_891_800.25, + request: { + url: 'http://127.0.0.1:43123/api?source=dynamic', + method: 'POST', + postData: '{"ok":true}', + hasPostData: true } - }) - - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - const storedRequest = result.getRequest('initiator-test-id') - expect(storedRequest).toBeDefined() - expect(storedRequest.initiator).toBeDefined() + } }) + expect(event.params.timestamp).toBeGreaterThan(0) + expect(event.params.timestamp).toBeLessThan(10_000_000_000) + expect(event.params.wallTime).toBeLessThan(10_000_000_000) + }) - test('处理带有 initiator 且 scriptId 存在的请求', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - const pluginResult = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先获取本地脚本列表,这会填充 resourceService - const scriptList = pluginResult.resourceService.getLocalScriptList() - - // 使用一个已知的脚本 URL - const knownScriptUrl = scriptList.length > 0 ? scriptList[0].url : '' - - if (knownScriptUrl) { - const testRequest = createTestRequest({ - id: 'initiator-scriptid-test-id', - initiator: { - type: 'script', - stack: { - callFrames: [ - { - columnNumber: 10, - functionName: 'testFunction', - lineNumber: 20, - url: knownScriptUrl.replace('file://', ''), - scriptId: '' - } - ] - } - } - }) - - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - const storedRequest = pluginResult.getRequest('initiator-scriptid-test-id') - expect(storedRequest).toBeDefined() - // scriptId 应该被设置 - if (storedRequest.initiator?.stack.callFrames[0]) { - expect(storedRequest.initiator.stack.callFrames[0].scriptId).toBeDefined() + test('emits responseReceived before data and the successful terminal event exactly once', async () => { + const detail = request({ + responseStatusCode: 201, + responseStatusText: 'Created', + responseHeaders: { 'content-type': 'application/json; charset=utf-8' }, + responseData: Buffer.from('{"created":true}'), + responseInfo: { dataLength: 16, encodedDataLength: 16 } + }) + + await handler('registerRequest')({ data: detail }) + await handler('responseReceived')({ data: detail }) + await handler('endRequest')({ data: detail }) + await handler('endRequest')({ data: detail }) + + expect(methods()).toEqual([ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.dataReceived', + 'Network.loadingFinished' + ]) + expect(messages()[1]).toMatchObject({ + params: { + type: 'Other', + response: { + status: 201, + statusText: 'Created', + mimeType: 'application/json', + charset: 'utf-8' } } }) + const timestamps = messages() + .map((message) => message.params?.timestamp) + .filter((value): value is number => typeof value === 'number') + expect(timestamps).toEqual([...timestamps].sort((a, b) => a - b)) }) - describe('registerRequest 处理器', () => { - test('注册请求并发送 Network.requestWillBeSent', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ - id: 'register-test-id', - url: 'http://example.com/api/data', - method: 'POST', - requestData: { key: 'value' }, - requestHeaders: { 'Content-Type': 'application/json' } - }) - - const registerRequestHandlers = registeredHandlers.get('registerRequest') - expect(registerRequestHandlers).toBeDefined() - registerRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 验证发送了 Network.requestWillBeSent - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.requestWillBeSent', - params: expect.objectContaining({ - requestId: 'register-test-id', - request: expect.objectContaining({ - url: 'http://example.com/api/data', - method: 'POST' - }) - }) - }) - ) - }) - - test('WebSocket 请求类型为 WebSocket', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ - id: 'ws-test-id', - url: 'ws://example.com/socket', - requestHeaders: { Upgrade: 'websocket' } - }) - - const registerRequestHandlers = registeredHandlers.get('registerRequest') - registerRequestHandlers![0]({ data: testRequest, id: undefined }) - - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.requestWillBeSent', - params: expect.objectContaining({ - type: 'WebSocket' - }) - }) - ) - }) - - test('普通请求类型为 Fetch', async () => { - const { networkPlugin } = await import('./index') + test('a pre-response failure emits only requestWillBeSent then loadingFailed', async () => { + const detail = request() - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ - id: 'fetch-test-id', - requestHeaders: { 'Content-Type': 'application/json' } - }) - - const registerRequestHandlers = registeredHandlers.get('registerRequest') - registerRequestHandlers![0]({ data: testRequest, id: undefined }) - - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.requestWillBeSent', - params: expect.objectContaining({ - type: 'Fetch' - }) - }) - ) + await handler('requestFailed')({ + data: { + request: detail, + errorText: 'ECONNREFUSED', + canceled: true, + blockedReason: 'other' + } }) - test('处理带有 initiator 且 scriptId 存在的 registerRequest', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - const pluginResult = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先获取本地脚本列表,这会填充 resourceService - const scriptList = pluginResult.resourceService.getLocalScriptList() - - // 使用一个已知的脚本 URL - const knownScriptUrl = scriptList.length > 0 ? scriptList[0].url : '' - - if (knownScriptUrl) { - const testRequest = createTestRequest({ - id: 'register-initiator-scriptid-test-id', - initiator: { - type: 'script', - stack: { - callFrames: [ - { - columnNumber: 10, - functionName: 'testFunction', - lineNumber: 20, - url: knownScriptUrl.replace('file://', ''), - scriptId: '' - } - ] - } - } - }) - - const registerRequestHandlers = registeredHandlers.get('registerRequest') - registerRequestHandlers![0]({ data: testRequest, id: undefined }) - - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.requestWillBeSent' - }) - ) + expect(methods()).toEqual(['Network.requestWillBeSent', 'Network.loadingFailed']) + expect(messages()[1]).toMatchObject({ + params: { + errorText: 'ECONNREFUSED', + canceled: true, + blockedReason: 'other' } }) + expect(methods()).not.toContain('Network.loadingFinished') + expect(methods()).not.toContain('Network.responseReceived') }) - describe('endRequest 处理器', () => { - test('结束请求并发送完整的响应消息序列', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - const testRequest = createTestRequest({ - id: 'end-test-id', - responseStatusCode: 200, - responseHeaders: { 'Content-Type': 'application/json' }, - responseInfo: { encodedDataLength: 100, dataLength: 100 } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - expect(endRequestHandlers).toBeDefined() - endRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 验证发送了完整的消息序列 - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived' - }) - ) - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.dataReceived' - }) - ) - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.loadingFinished' - }) - ) - }) - - test('根据 content-type 确定响应类型', async () => { - const { networkPlugin } = await import('./index') - - // 测试 Image 类型 - const mockDevtool1 = createMockDevtool() - const mockCore1 = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool1, - core: mockCore1, - plugins: [] - }) - - const imageRequest = createTestRequest({ - id: 'image-test-id', - responseHeaders: { 'Content-Type': 'image/png' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: imageRequest, id: undefined }) - - expect(mockDevtool1.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'Image' - }) - }) - ) - }) - - test('JavaScript 响应类型为 Script', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const jsRequest = createTestRequest({ - id: 'js-test-id', - responseHeaders: { 'Content-Type': 'application/javascript' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: jsRequest, id: undefined }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'Script' - }) - }) - ) - }) - - test('CSS 响应类型为 Stylesheet', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const cssRequest = createTestRequest({ - id: 'css-test-id', - responseHeaders: { 'Content-Type': 'text/css' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: cssRequest, id: undefined }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'Stylesheet' - }) - }) - ) - }) - - test('HTML 响应类型为 Document', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const htmlRequest = createTestRequest({ - id: 'html-test-id', - responseHeaders: { 'Content-Type': 'text/html' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: htmlRequest, id: undefined }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'Document' - }) - }) - ) + test('a post-header failure keeps responseReceived but never emits loadingFinished', async () => { + const detail = request({ + responseStatusCode: 502, + responseStatusText: 'Bad Gateway', + responseHeaders: { 'content-type': 'text/plain' } }) - test('未知类型响应为 Other', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() + await handler('registerRequest')({ data: detail }) + await handler('requestFailed')({ data: { request: detail, errorText: 'stream reset' } }) - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const otherRequest = createTestRequest({ - id: 'other-test-id', - responseHeaders: { 'Content-Type': 'application/octet-stream' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: otherRequest, id: undefined }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'Other' - }) - }) - ) - }) + expect(methods()).toEqual([ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.loadingFailed' + ]) + expect(methods()).not.toContain('Network.loadingFinished') }) - describe('Network.getResponseBody 处理器', () => { - test('返回请求的响应体', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - const result = networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先初始化一个请求 - const testRequest = createTestRequest({ - id: 'body-test-id', - responseData: Buffer.from('{"message":"hello"}'), - responseHeaders: { 'Content-Type': 'application/json' } - }) - - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 请求响应体 - const getResponseBodyHandlers = registeredHandlers.get('Network.getResponseBody') - expect(getResponseBodyHandlers).toBeDefined() - getResponseBodyHandlers![0]({ data: { requestId: 'body-test-id' }, id: 'response-id-1' }) - - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'response-id-1', - result: expect.objectContaining({ - body: expect.any(String), - base64Encoded: expect.any(Boolean) - }) - }) - ) + test('decodes compressed response DTOs and exposes body through async result', async () => { + const detail = request() + const body = Buffer.from('compressed response') + await handler('registerRequest')({ data: detail }) + await handler('responseData')({ + data: { + id: detail.id, + rawData: zlib.gzipSync(body), + statusCode: 200, + statusMessage: 'OK', + headers: { 'content-type': 'text/plain; charset=utf-8' }, + contentEncoding: 'gzip' + } }) + const result = vi.fn().mockResolvedValue(undefined) + const error = vi.fn().mockResolvedValue(undefined) - test('请求不存在时不发送响应', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - // 请求不存在的响应体 - const getResponseBodyHandlers = registeredHandlers.get('Network.getResponseBody') - getResponseBodyHandlers![0]({ data: { requestId: 'non-existent-id' }, id: 'response-id-2' }) - - expect(consoleErrorSpy).toHaveBeenCalledWith('request is not found') - - consoleErrorSpy.mockRestore() + await handler('Network.getResponseBody')({ + data: { requestId: detail.id }, + id: 0, + result, + error }) - test('没有 id 时不发送响应', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - // 没有 id 的请求 - const getResponseBodyHandlers = registeredHandlers.get('Network.getResponseBody') - getResponseBodyHandlers![0]({ data: { requestId: 'some-id' }, id: undefined }) - - expect(consoleErrorSpy).toHaveBeenCalledWith('request is not found') - - consoleErrorSpy.mockRestore() - }) + expect(methods()).toEqual([ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.dataReceived', + 'Network.loadingFinished' + ]) + expect(result).toHaveBeenCalledWith({ body: 'compressed response', base64Encoded: false }) + expect(error).not.toHaveBeenCalled() }) - describe('responseData 处理器', () => { - test('处理响应数据并结束请求', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先初始化一个请求 - const testRequest = createTestRequest({ id: 'response-data-test-id' }) - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 发送响应数据 - const responseDataHandlers = registeredHandlers.get('responseData') - expect(responseDataHandlers).toBeDefined() - - const rawData = Array.from(Buffer.from('{"result":"success"}')) - responseDataHandlers![0]({ - data: { - id: 'response-data-test-id', - rawData, - statusCode: 200, - headers: { 'Content-Type': 'application/json' } - }, - id: undefined - }) - - // 等待异步解压缩完成 - await vi.waitFor(() => { - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived' - }) - ) - }) - }) - - test('请求不存在时不处理响应数据', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 发送不存在请求的响应数据 - const responseDataHandlers = registeredHandlers.get('responseData') - const rawData = Array.from(Buffer.from('test')) - responseDataHandlers![0]({ - data: { - id: 'non-existent-id', - rawData, - statusCode: 200, - headers: {} - }, - id: undefined - }) - - // 不应该发送任何消息 - expect(mockDevtoolSend).not.toHaveBeenCalled() - }) - - test('处理 gzip 压缩的响应数据', async () => { - const zlib = await import('zlib') - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先初始化一个请求 - const testRequest = createTestRequest({ id: 'gzip-test-id' }) - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 创建 gzip 压缩的数据 - const originalData = '{"compressed":"data"}' - const compressedData = zlib.gzipSync(Buffer.from(originalData)) - - const responseDataHandlers = registeredHandlers.get('responseData') - responseDataHandlers![0]({ - data: { - id: 'gzip-test-id', - rawData: Array.from(compressedData), - statusCode: 200, - headers: { 'Content-Encoding': 'gzip' } - }, - id: undefined - }) - - // 等待异步解压缩完成 - await vi.waitFor(() => { - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived' - }) - ) - }) - }) - - test('处理 deflate 压缩的响应数据', async () => { - const zlib = await import('zlib') - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先初始化一个请求 - const testRequest = createTestRequest({ id: 'deflate-test-id' }) - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 创建 deflate 压缩的数据 - const originalData = '{"deflate":"data"}' - const compressedData = zlib.deflateSync(Buffer.from(originalData)) - - const responseDataHandlers = registeredHandlers.get('responseData') - responseDataHandlers![0]({ - data: { - id: 'deflate-test-id', - rawData: Array.from(compressedData), - statusCode: 200, - headers: { 'Content-Encoding': 'deflate' } - }, - id: undefined - }) - - // 等待异步解压缩完成 - await vi.waitFor(() => { - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived' - }) - ) - }) - }) - - test('处理 brotli 压缩的响应数据', async () => { - const zlib = await import('zlib') - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [networkPlugin] - }) - - // 先初始化一个请求 - const testRequest = createTestRequest({ id: 'brotli-test-id' }) - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 清除之前的调用记录 - mockDevtoolSend.mockClear() - - // 创建 brotli 压缩的数据 - const originalData = '{"brotli":"data"}' - const compressedData = zlib.brotliCompressSync(Buffer.from(originalData)) - - const responseDataHandlers = registeredHandlers.get('responseData') - responseDataHandlers![0]({ - data: { - id: 'brotli-test-id', - rawData: Array.from(compressedData), - statusCode: 200, - headers: { 'Content-Encoding': 'br' } - }, - id: undefined - }) - - // 等待异步解压缩完成 - await vi.waitFor(() => { - expect(mockDevtoolSend).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived' - }) - ) - }) - }) + test('returns standard command errors for invalid or unfinished body requests', async () => { + const detail = request() + await handler('initRequest')({ data: detail }) + const error = vi.fn().mockResolvedValue(undefined) + + await handler('Network.getResponseBody')({ data: {}, error }) + expect(error).toHaveBeenLastCalledWith( + CDP_ERROR_CODES.INVALID_PARAMS, + 'requestId must be a string.' + ) + + await handler('Network.getResponseBody')({ data: { requestId: detail.id }, error }) + expect(error).toHaveBeenLastCalledWith( + CDP_ERROR_CODES.SERVER_ERROR, + `No finished request with id ${detail.id}.` + ) }) - describe('endRequest 边界情况', () => { - test('没有 content-type 时使用默认值', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const testRequest = createTestRequest({ - id: 'no-content-type-id', - responseHeaders: {} // 没有 content-type - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: testRequest, id: undefined }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - response: expect.objectContaining({ - mimeType: 'text/plain' - }) - }) - }) - ) - }) - - test('没有 requestEndTime 时使用当前时间', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const testRequest = createTestRequest({ - id: 'no-end-time-id', - requestEndTime: undefined - }) - - const beforeTime = Date.now() - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: testRequest, id: undefined }) - const afterTime = Date.now() - - // 验证 requestEndTime 被设置 - expect(mockDevtool.send).toHaveBeenCalled() - }) + test('returns captured post data or a server error through command callbacks', async () => { + const withBody = request({ requestData: new Uint8Array(Buffer.from('a=1')) }) + const withoutBody = request() + await handler('initRequest')({ data: withBody }) + await handler('initRequest')({ data: withoutBody }) + const result = vi.fn().mockResolvedValue(undefined) + const error = vi.fn().mockResolvedValue(undefined) + + await handler('Network.getRequestPostData')({ + data: { requestId: withBody.id }, + result, + error + }) + await handler('Network.getRequestPostData')({ + data: { requestId: withoutBody.id }, + result, + error + }) + + expect(result).toHaveBeenCalledWith({ postData: 'a=1' }) + expect(error).toHaveBeenCalledWith( + CDP_ERROR_CODES.SERVER_ERROR, + `No request body for ${withoutBody.id}.` + ) }) - describe('Server-Sent Events (SSE) 支持', () => { - test('eventSourceMessage 处理器发送 CDP 消息', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - // 先注册一个请求 - const testRequest = createTestRequest({ - id: 'sse-request-id', - url: 'http://example.com/sse' - }) - - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 发送 eventSourceMessage - const eventSourceHandlers = registeredHandlers.get('eventSourceMessage') - expect(eventSourceHandlers).toBeDefined() - - eventSourceHandlers![0]({ - data: { - requestId: 'sse-request-id', - eventName: 'message', - eventId: '1', - data: 'Hello SSE' - }, - id: undefined - }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.eventSourceMessageReceived', - params: expect.objectContaining({ - requestId: 'sse-request-id', - eventName: 'message', - eventId: '1', - data: 'Hello SSE' - }) - }) - ) + test('emits SSE response, messages, and terminal events in protocol order', async () => { + const detail = request({ + responseStatusCode: 200, + responseStatusText: 'OK', + responseHeaders: { 'content-type': 'text/event-stream; charset=utf-8' }, + responseData: Buffer.from('event: update\ndata: one\n\n'), + responseInfo: { dataLength: 25, encodedDataLength: 25 } + }) + + await handler('registerRequest')({ data: detail }) + await handler('eventSourceResponseReceived')({ data: detail }) + await handler('eventSourceMessage')({ + data: { requestId: detail.id, eventName: 'update', eventId: '42', data: 'one\ntwo' } + }) + await handler('endRequest')({ data: detail }) + + expect(methods()).toEqual([ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.eventSourceMessageReceived', + 'Network.dataReceived', + 'Network.loadingFinished' + ]) + expect(messages()[1].params.type).toBe('EventSource') + expect(messages()[2].params).toMatchObject({ + eventName: 'update', + eventId: '42', + data: 'one\ntwo' }) + }) - test('eventSourceMessage 处理自定义事件类型', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - // 先注册一个请求 - const testRequest = createTestRequest({ - id: 'sse-custom-event-id', - url: 'http://example.com/sse' - }) - - const initRequestHandlers = registeredHandlers.get('initRequest') - initRequestHandlers![0]({ data: testRequest, id: undefined }) - - // 发送自定义事件类型的 eventSourceMessage - const eventSourceHandlers = registeredHandlers.get('eventSourceMessage') - - eventSourceHandlers![0]({ - data: { - requestId: 'sse-custom-event-id', - eventName: 'customEvent', - eventId: '42', - data: '{"status": "complete"}' - }, - id: undefined - }) - - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.eventSourceMessageReceived', - params: expect.objectContaining({ - requestId: 'sse-custom-event-id', - eventName: 'customEvent', - eventId: '42', - data: '{"status": "complete"}' - }) - }) - ) + test('lazily registers and announces an initiator script before its request', async () => { + const scriptPath = path.join(fixtureDir, 'initiator.js') + fs.writeFileSync(scriptPath, 'export const initiator = true') + const detail = request({ + initiator: { + type: 'script', + stack: { + callFrames: [ + { + functionName: 'callApi', + url: scriptPath, + lineNumber: 1, + columnNumber: 2 + } + ] + } + } }) - test('eventSourceMessage 对未知请求 ID 不发送消息', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) + await handler('registerRequest')({ data: detail }) + await handler('registerRequest')({ data: detail }) - // 不注册请求,直接发送 eventSourceMessage - const eventSourceHandlers = registeredHandlers.get('eventSourceMessage') - - eventSourceHandlers![0]({ - data: { - requestId: 'unknown-request-id', - eventName: 'message', - eventId: '', - data: 'test' - }, - id: undefined - }) - - // 不应该发送 eventSourceMessageReceived - const eventSourceCalls = mockDevtool.send.mock.calls.filter( - (call: unknown[]) => - (call[0] as { method?: string })?.method === 'Network.eventSourceMessageReceived' - ) - expect(eventSourceCalls.length).toBe(0) + expect(methods()).toEqual(['Debugger.scriptParsed', 'Network.requestWillBeSent']) + expect(messages()[0].params).toMatchObject({ + scriptId: '1', + scriptLanguage: 'JavaScript' }) + expect(detail.initiator!.stack.callFrames[0].scriptId).toBe('1') + expect(plugin.resourceService.getLocalScriptList()).toHaveLength(1) + }) - test('text/event-stream 类型被识别为 EventSource', async () => { - const { networkPlugin } = await import('./index') - - const mockDevtool = createMockDevtool() - const mockCore = createMockCore() - registeredHandlers.clear() - - networkPlugin({ - devtool: mockDevtool, - core: mockCore, - plugins: [] - }) - - const testRequest = createTestRequest({ - id: 'sse-type-test-id', - responseHeaders: { 'content-type': 'text/event-stream' } - }) - - const endRequestHandlers = registeredHandlers.get('endRequest') - endRequestHandlers![0]({ data: testRequest, id: undefined }) + test.each([ + ['image/png', 'Image'], + ['application/javascript', 'Script'], + ['text/css', 'Stylesheet'], + ['text/html', 'Document'], + ['application/octet-stream', 'Other'] + ])('maps %s responses to %s', async (contentType, type) => { + const detail = request({ + responseStatusCode: 200, + responseHeaders: { 'content-type': contentType } + }) + await handler('responseReceived')({ data: detail }) + expect( + messages().find((message) => message.method === 'Network.responseReceived')?.params.type + ).toBe(type) + }) +}) - expect(mockDevtool.send).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'Network.responseReceived', - params: expect.objectContaining({ - type: 'EventSource' - }) - }) - ) - }) +describe('toMimeType', () => { + test.each([ + ['application/json; charset=utf-8', 'application/json'], + ['text/plain', 'text/plain'], + ['', 'text/plain'] + ])('normalizes %j', (input, expected) => { + expect(toMimeType(input)).toBe(expected) }) }) diff --git a/packages/network-debugger/src/fork/module/network/index.ts b/packages/network-debugger/src/fork/module/network/index.ts index e37732d..f285be7 100644 --- a/packages/network-debugger/src/fork/module/network/index.ts +++ b/packages/network-debugger/src/fork/module/network/index.ts @@ -1,283 +1,374 @@ -import { pathToFileURL } from 'url' +import { pathToFileURL } from 'node:url' +import zlib from 'node:zlib' +import { promisify } from 'node:util' import { RequestDetail } from '../../../common' +import type { LegacyRequestFailure, LegacyResponseData } from '../../../legacy-bridge/contracts' import { BodyTransformer, RequestHeaderPipe } from '../../pipe' -import { createPlugin, useHandler } from '../common' -import zlib from 'node:zlib' import { ResourceService } from '../../resource-service' +import { CDP_ERROR_CODES } from '../../devtool' +import { createPlugin, useHandler } from '../common' -const frameId = '517.528' -const loaderId = '517.529' +const gunzip = promisify(zlib.gunzip) +const inflate = promisify(zlib.inflate) +const brotliDecompress = promisify(zlib.brotliDecompress) +const frameId = 'nnd.legacy.frame' +const loaderId = 'nnd.legacy.loader' +const MAX_RETAINED_REQUESTS = 1_000 -export const toMimeType = (contentType: string) => { - return contentType.split(';')[0] || 'text/plain' +interface RequestState { + request: RequestDetail + requestSent: boolean + responseSent: boolean + terminal?: 'finished' | 'failed' } -export const networkPlugin = createPlugin('network', ({ devtool, core }) => { - const requests: Record = {} +export const toMimeType = (contentType: string) => contentType.split(';')[0] || 'text/plain' + +function resourceType(contentType: string, eventSource = false) { + if (eventSource || /text\/event-stream/i.test(contentType)) return 'EventSource' + if (/image/i.test(contentType)) return 'Image' + if (/javascript/i.test(contentType)) return 'Script' + if (/css/i.test(contentType)) return 'Stylesheet' + if (/html/i.test(contentType)) return 'Document' + return 'Other' +} + +function wallTime(value: number | undefined): number { + if (!value) return Date.now() / 1000 + return value > 10_000_000_000 ? value / 1000 : value +} +function statusText(request: RequestDetail): string { + if (request.responseStatusText) return request.responseStatusText + if (request.responseStatusCode === 200) return 'OK' + return '' +} + +function bufferFrom(value: unknown): Buffer | undefined { + if (Buffer.isBuffer(value)) return value + if (value instanceof Uint8Array) return Buffer.from(value) + if ( + value && + typeof value === 'object' && + 'type' in value && + value.type === 'Buffer' && + 'data' in value && + Array.isArray(value.data) + ) { + return Buffer.from(value.data) + } + return undefined +} + +async function decodeContent(data: Buffer, encoding: string | undefined): Promise { + const normalized = encoding?.split(',')[0]?.trim().toLowerCase() + try { + if (normalized === 'gzip' || normalized === 'x-gzip') return await gunzip(data) + if (normalized === 'deflate') return await inflate(data) + if (normalized === 'br') return await brotliDecompress(data) + } catch { + // Preserve the actual bytes if a server declared a bad encoding. Capture + // must not turn a successful application response into a protocol failure. + } + return data +} + +function requestPostData(request: RequestDetail): string | undefined { + if (request.requestData === undefined || request.requestData === null) return undefined + const buffer = bufferFrom(request.requestData) + return buffer ? buffer.toString('utf8') : String(request.requestData) +} + +export const networkPlugin = createPlugin('network', ({ devtool }) => { + const states = new Map() const resourceService = new ResourceService() + const announcedScripts = new Set() - const getRequest = (id: string) => requests[id] - const updateRequest = (request: RequestDetail) => { - requests[request.id] = request + const stateFor = (input: RequestDetail): RequestState => { + let state = states.get(input.id) + const request = new RequestDetail(input) + if (!state) { + state = { request, requestSent: false, responseSent: false } + states.set(input.id, state) + } else { + state.request = request + } + return state } - const endRequest = (request: RequestDetail) => { - requests[request.id] = request - request.requestEndTime = request.requestEndTime || Date.now() - devtool.updateTimestamp() - const headers = new RequestHeaderPipe(request.responseHeaders) - const contentType = headers.getHeader('content-type') || 'text/plain; charset=utf-8' + const trim = () => { + if (states.size <= MAX_RETAINED_REQUESTS) return + for (const [id, state] of states) { + if (!state.terminal) continue + states.delete(id) + if (states.size <= MAX_RETAINED_REQUESTS) break + } + } - const type = (() => { - if (/text\/event-stream/.test(contentType)) { - return 'EventSource' - } - if (/image/.test(contentType)) { - return 'Image' - } - if (/javascript/.test(contentType)) { - return 'Script' + const mapInitiatorScripts = async (request: RequestDetail) => { + for (const frame of request.initiator?.stack.callFrames ?? []) { + let url = frame.url + if (url && !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url)) { + try { + url = pathToFileURL(url).href + } catch { + // Keep the original call-frame URL. + } } - if (/css/.test(contentType)) { - return 'Stylesheet' + const scriptId = + resourceService.getScriptIdByUrl(url) ?? resourceService.getScriptIdByUrl(frame.url) + if (scriptId) { + frame.scriptId = scriptId + if (!announcedScripts.has(scriptId)) { + const descriptor = resourceService + .getLocalScriptList() + .find((script) => script.scriptId === scriptId) + if (descriptor) { + announcedScripts.add(scriptId) + await devtool.send({ method: 'Debugger.scriptParsed', params: { ...descriptor } }) + } + } } - if (/html/.test(contentType)) { - return 'Document' + } + } + + const emitRequest = async (state: RequestState) => { + if (state.requestSent) return + state.requestSent = true + const request = state.request + await mapInitiatorScripts(request) + const headers = new RequestHeaderPipe(request.requestHeaders).getData() + const postData = requestPostData(request) + await devtool.send({ + method: 'Network.requestWillBeSent', + params: { + requestId: request.id, + frameId, + loaderId, + documentURL: request.url, + request: { + url: request.url, + method: request.method ?? 'GET', + headers, + initialPriority: 'High', + mixedContentType: 'none', + ...(postData !== undefined ? { postData, hasPostData: true } : {}) + }, + timestamp: devtool.getTimestamp(), + wallTime: wallTime(request.requestStartTime), + initiator: request.initiator ?? { type: 'other' }, + type: request.isWebSocket() ? 'WebSocket' : 'Fetch' } - return 'Other' - })() + }) + } - devtool.send({ + const emitResponse = async (state: RequestState, eventSource = false) => { + if (state.responseSent) return + if (!state.requestSent) await emitRequest(state) + state.responseSent = true + const request = state.request + const headers = new RequestHeaderPipe(request.responseHeaders).getData() + const contentType = String( + new RequestHeaderPipe(headers).getHeader('content-type') ?? 'text/plain; charset=utf-8' + ) + await devtool.send({ method: 'Network.responseReceived', params: { requestId: request.id, frameId, loaderId, - timestamp: devtool.timestamp, - type, + timestamp: devtool.getTimestamp(), + type: resourceType(contentType, eventSource), response: { url: request.url, - status: request.responseStatusCode, - statusText: request.responseStatusCode === 200 ? 'OK' : '', - headers: request.responseHeaders, + status: request.responseStatusCode ?? 0, + statusText: statusText(request), + headers, connectionReused: false, - encodedDataLength: request.responseInfo.encodedDataLength, - charset: 'utf-8', + encodedDataLength: request.responseInfo.encodedDataLength ?? 0, + charset: /charset=([^;]+)/i.exec(contentType)?.[1] ?? 'utf-8', mimeType: toMimeType(contentType) } } }) + } - devtool.updateTimestamp() - devtool.send({ - method: 'Network.dataReceived', + const finish = async (state: RequestState) => { + if (state.terminal) return + await emitResponse(state) + const request = state.request + const dataLength = + request.responseInfo.dataLength ?? bufferFrom(request.responseData)?.length ?? 0 + const encodedDataLength = request.responseInfo.encodedDataLength ?? dataLength + if (dataLength > 0 || encodedDataLength > 0) { + await devtool.send({ + method: 'Network.dataReceived', + params: { + requestId: request.id, + timestamp: devtool.getTimestamp(), + dataLength, + encodedDataLength + } + }) + } + state.terminal = 'finished' + await devtool.send({ + method: 'Network.loadingFinished', params: { requestId: request.id, - timestamp: devtool.timestamp, - dataLength: request.responseInfo.dataLength, - encodedDataLength: request.responseInfo.encodedDataLength + timestamp: devtool.getTimestamp(), + encodedDataLength } }) + trim() + } - devtool.updateTimestamp() - devtool.send({ - method: 'Network.loadingFinished', + const fail = async (failure: LegacyRequestFailure) => { + const state = stateFor(failure.request) + if (state.terminal) return + if (!state.requestSent) await emitRequest(state) + if ((state.request.responseStatusCode ?? 0) > 0) await emitResponse(state) + state.terminal = 'failed' + await devtool.send({ + method: 'Network.loadingFailed', params: { - requestId: request.id, - timestamp: devtool.timestamp, - encodedDataLength: request.responseInfo.encodedDataLength + requestId: state.request.id, + timestamp: devtool.getTimestamp(), + type: 'Fetch', + errorText: failure.errorText || 'Request failed', + canceled: Boolean(failure.canceled), + ...(failure.blockedReason ? { blockedReason: failure.blockedReason } : {}) } }) + trim() } - const tryDecompression = (data: Buffer, callback: (result: Buffer) => void) => { - const decompressors: Array< - (data: Buffer, cb: (err: Error | null, result: Buffer) => void) => void - > = [zlib.gunzip, zlib.inflate, zlib.brotliDecompress] - - let attempts = 0 - - const tryNext = () => { - if (attempts >= decompressors.length) { - callback(data) // 理论上没有压缩 + useHandler<{ requestId?: unknown }>( + 'Network.getResponseBody', + async ({ data, result, error }) => { + if (typeof data?.requestId !== 'string') { + await error?.(CDP_ERROR_CODES.INVALID_PARAMS, 'requestId must be a string.') return } - - const decompressor = decompressors[attempts] - attempts += 1 - - decompressor(data, (err, result) => { - if (!err) { - callback(result) - } else { - tryNext() - } - }) + const state = states.get(data.requestId) + if (!state || state.terminal !== 'finished') { + await error?.( + CDP_ERROR_CODES.SERVER_ERROR, + `No finished request with id ${data.requestId}.` + ) + return + } + const body = new BodyTransformer(state.request).decodeBody() + await result?.({ body: body.body ?? '', base64Encoded: body.base64Encoded }) } + ) - tryNext() - } - - useHandler<{ requestId: string }>('Network.getResponseBody', ({ data, id }) => { - const request = getRequest(data.requestId) - if (!id || !request) { - console.error('request is not found') - return + useHandler<{ requestId?: unknown }>( + 'Network.getRequestPostData', + async ({ data, result, error }) => { + if (typeof data?.requestId !== 'string') { + await error?.(CDP_ERROR_CODES.INVALID_PARAMS, 'requestId must be a string.') + return + } + const state = states.get(data.requestId) + const postData = state ? requestPostData(state.request) : undefined + if (postData === undefined) { + await error?.(CDP_ERROR_CODES.SERVER_ERROR, `No request body for ${data.requestId}.`) + return + } + await result?.({ postData }) } - const body = new BodyTransformer(request).decodeBody() - - devtool.send({ - id, - result: body - }) - }) + ) useHandler('initRequest', ({ data }) => { - const request = new RequestDetail(data) - if (request.initiator) { - request.initiator.stack.callFrames.forEach((frame) => { - const fileUrl = pathToFileURL(frame.url) - const scriptId = - resourceService.getScriptIdByUrl(fileUrl.href) ?? - resourceService.getScriptIdByUrl(frame.url) - if (scriptId) { - frame.scriptId = scriptId - } - }) - } - requests[request.id] = request + stateFor(data) }) - useHandler('registerRequest', ({ data }) => { - const request = new RequestDetail(data) - - requests[request.id] = request - // replace callFrames' scriptId - if (request.initiator) { - request.initiator.stack.callFrames.forEach((frame) => { - const fileUrl = pathToFileURL(frame.url) - const scriptId = - resourceService.getScriptIdByUrl(fileUrl.href) ?? - resourceService.getScriptIdByUrl(frame.url) - if (scriptId) { - frame.scriptId = scriptId - } - }) - } - devtool.updateTimestamp() - - const headerPipe = new RequestHeaderPipe(request.requestHeaders) - const contentType = headerPipe.getHeader('content-type') + useHandler('registerRequest', async ({ data }) => { + const state = stateFor(data) + await emitRequest(state) + }) - return devtool.send({ - method: 'Network.requestWillBeSent', - params: { - requestId: request.id, - frameId, - loaderId, - request: { - url: request.url, - method: request.method, - headers: headerPipe.getData(), - initialPriority: 'High', - mixedContentType: 'none', - ...(request.requestData ? { postData: request.requestData.toString() } : {}) - }, - timestamp: devtool.timestamp, - wallTime: request.requestStartTime, - initiator: request.initiator, - type: request.isWebSocket() ? 'WebSocket' : 'Fetch' - } - }) + useHandler('updateRequest', ({ data }) => { + stateFor(data) }) - useHandler('endRequest', ({ data }) => { - const request = new RequestDetail(data) - endRequest(request) + useHandler('responseReceived', async ({ data }) => { + const state = stateFor(data) + await emitResponse(state) }) - // Handle SSE response received - send responseReceived with type: EventSource before streaming messages - useHandler('eventSourceResponseReceived', ({ data }) => { - const request = new RequestDetail(data) - requests[request.id] = request - devtool.updateTimestamp() - const headers = new RequestHeaderPipe(request.responseHeaders) - const contentType = headers.getHeader('content-type') || 'text/event-stream' + useHandler('eventSourceResponseReceived', async ({ data }) => { + const state = stateFor(data) + await emitResponse(state, true) + }) - devtool.send({ - method: 'Network.responseReceived', - params: { - requestId: request.id, - frameId, - loaderId, - timestamp: devtool.timestamp, - type: 'EventSource', - response: { - url: request.url, - status: request.responseStatusCode, - statusText: request.responseStatusCode === 200 ? 'OK' : '', - headers: request.responseHeaders, - connectionReused: false, - encodedDataLength: 0, - charset: 'utf-8', - mimeType: toMimeType(contentType) - } - } - }) + useHandler('endRequest', async ({ data }) => { + const state = stateFor(data) + if ((state.request.responseStatusCode ?? 0) === 0) { + await fail({ request: state.request, errorText: 'Request failed' }) + return + } + const body = bufferFrom(state.request.responseData) ?? Buffer.alloc(0) + state.request.responseData = body + state.request.responseInfo = { + dataLength: state.request.responseInfo.dataLength ?? body.length, + encodedDataLength: state.request.responseInfo.encodedDataLength ?? body.length + } + await finish(state) }) - useHandler<{ - id: string - rawData: Array - statusCode: number - headers: Record - }>('responseData', ({ data }) => { - const { id, rawData: _rawData, statusCode, headers } = data - const request = getRequest(id) - const rawData = Buffer.from(_rawData) - if (request) { - request.responseInfo.encodedDataLength = rawData.length - tryDecompression(rawData, (decodedData) => { - request.responseData = decodedData - request.responseInfo.dataLength = decodedData.length - request.responseStatusCode = statusCode - request.responseHeaders = new RequestHeaderPipe(headers).getData() - updateRequest(request) - endRequest(request) - }) + useHandler('responseData', async ({ data }) => { + const state = states.get(data.id) + if (!state || state.terminal) return + const raw = bufferFrom(data.rawData) ?? Buffer.alloc(0) + const decoded = await decodeContent(raw, data.contentEncoding) + state.request.responseData = decoded + state.request.responseStatusCode = data.statusCode + state.request.responseStatusText = data.statusMessage + state.request.responseHeaders = new RequestHeaderPipe(data.headers).getData() + state.request.responseInfo = { + dataLength: decoded.length, + encodedDataLength: raw.length } + await finish(state) + }) + + useHandler('requestFailed', async ({ data }) => { + await fail(data) }) - // Handle Server-Sent Events (SSE) messages useHandler<{ requestId: string eventName: string eventId: string data: string - }>('eventSourceMessage', ({ data }) => { - const { requestId, eventName, eventId, data: eventData } = data - const request = getRequest(requestId) - if (!request) { - return - } - - devtool.updateTimestamp() - devtool.send({ + }>('eventSourceMessage', async ({ data }) => { + const state = states.get(data.requestId) + if (!state || state.terminal) return + await devtool.send({ method: 'Network.eventSourceMessageReceived', params: { - requestId, - timestamp: devtool.timestamp, - eventName, - eventId, - data: eventData + requestId: data.requestId, + timestamp: devtool.getTimestamp(), + eventName: data.eventName, + eventId: data.eventId, + data: data.data } }) }) return { - getRequest, - resourceService + getRequest(id: string) { + return states.get(id)?.request + }, + removeRequest(id: string) { + states.delete(id) + }, + resourceService, + requestCount() { + return states.size + } } }) diff --git a/packages/network-debugger/src/fork/module/websocket/index.test.ts b/packages/network-debugger/src/fork/module/websocket/index.test.ts index aec1d3d..9f5f516 100644 --- a/packages/network-debugger/src/fork/module/websocket/index.test.ts +++ b/packages/network-debugger/src/fork/module/websocket/index.test.ts @@ -4,31 +4,38 @@ import type { DevtoolMessageListener } from '../../request-center' import type { IncomingMessage } from 'http' // 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { mockCoreOn, mockDevtoolSend, registeredHandlers, mockUsePlugin, mockGetRequest } = - vi.hoisted(() => { - const handlers = new Map[]>() - return { - mockCoreOn: vi.fn((type: string, fn: DevtoolMessageListener) => { - if (!handlers.has(type)) { - handlers.set(type, []) - } - handlers.get(type)!.push(fn) - return () => { - const list = handlers.get(type) - if (list) { - const index = list.indexOf(fn) - if (index > -1) { - list.splice(index, 1) - } +const { + mockCoreOn, + mockDevtoolSend, + registeredHandlers, + mockUsePlugin, + mockGetRequest, + mockRemoveRequest +} = vi.hoisted(() => { + const handlers = new Map[]>() + return { + mockCoreOn: vi.fn((type: string, fn: DevtoolMessageListener) => { + if (!handlers.has(type)) { + handlers.set(type, []) + } + handlers.get(type)!.push(fn) + return () => { + const list = handlers.get(type) + if (list) { + const index = list.indexOf(fn) + if (index > -1) { + list.splice(index, 1) } } - }), - mockDevtoolSend: vi.fn().mockResolvedValue(undefined), - registeredHandlers: handlers, - mockUsePlugin: vi.fn(), - mockGetRequest: vi.fn() - } - }) + } + }), + mockDevtoolSend: vi.fn().mockResolvedValue(undefined), + registeredHandlers: handlers, + mockUsePlugin: vi.fn(), + mockGetRequest: vi.fn(), + mockRemoveRequest: vi.fn() + } +}) // Mock DevtoolServer 和 RequestCenter 模块 vi.mock('../../devtool', () => ({ @@ -113,6 +120,7 @@ describe('fork/module/websocket/index.ts', () => { // 设置 mockUsePlugin 返回 network 插件的 mock mockUsePlugin.mockReturnValue({ getRequest: mockGetRequest, + removeRequest: mockRemoveRequest, resourceService: { getScriptSource: vi.fn(), getLocalScriptList: vi.fn().mockReturnValue([]), @@ -489,6 +497,7 @@ describe('fork/module/websocket/index.ts', () => { requestId: 'ws-closed-test' }) }) + expect(mockRemoveRequest).toHaveBeenCalledWith('ws-closed-test') }) test('没有 requestId 时不发送', async () => { diff --git a/packages/network-debugger/src/fork/module/websocket/index.ts b/packages/network-debugger/src/fork/module/websocket/index.ts index 8ab9853..165f2a0 100644 --- a/packages/network-debugger/src/fork/module/websocket/index.ts +++ b/packages/network-debugger/src/fork/module/websocket/index.ts @@ -1,33 +1,15 @@ -import { IncomingMessage } from 'http' -import { CDPCallFrame } from '../../../common' -import { - formatHeadersToHeaderText, - getTimestamp, - parseRawHeaders, - stringifyNestedObj -} from '../../../utils' +import type { + LegacyWebSocketFrame, + LegacyWebSocketHandshake +} from '../../../legacy-bridge/contracts' +import { formatHeadersToHeaderText, parseRawHeaders, stringifyNestedObj } from '../../../utils' import { createPlugin, useHandler } from '../common' import { NetworkPluginCore } from '../network' -export interface WebSocketFrameSent { - requestId: string - response: { - payloadData: string - opcode: number - mask: boolean - } -} - export interface WebSocketCreated { requestId: string url: string - initiator: { - type: string - stack: { - callFrames: CDPCallFrame[] - } - } - response: IncomingMessage + response: LegacyWebSocketHandshake } export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => { @@ -44,7 +26,7 @@ export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => return } - const convertedResponseHeaders = parseRawHeaders(response.rawHeaders) + const convertedResponseHeaders = parseRawHeaders([...response.rawHeaders]) await devtool.send({ method: 'Network.webSocketCreated', @@ -57,7 +39,7 @@ export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => await devtool.send({ method: 'Network.webSocketWillSendHandshakeRequest', params: { - wallTime: Date.now(), + wallTime: Date.now() / 1000, timestamp: devtool.getTimestamp(), requestId: request.id, request: { @@ -90,7 +72,7 @@ export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => } ) - useHandler('Network.webSocketFrameSent', async ({ data }) => { + useHandler('Network.webSocketFrameSent', async ({ data }) => { if (!data.requestId) { return } @@ -101,12 +83,12 @@ export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => requestId: data.requestId, response: data.response, // Network中数据时间戳 - timestamp: getTimestamp() + timestamp: devtool.getTimestamp() } }) }) - useHandler('Network.webSocketFrameReceived', async ({ data }) => { + useHandler('Network.webSocketFrameReceived', async ({ data }) => { if (!data.requestId) { return } @@ -115,21 +97,25 @@ export const websocketPlugin = createPlugin('websocket', ({ devtool, core }) => params: { requestId: data.requestId, response: data.response, - timestamp: getTimestamp() + timestamp: devtool.getTimestamp() } }) }) - useHandler('Network.webSocketClosed', async ({ data }) => { + useHandler<{ requestId: string }>('Network.webSocketClosed', async ({ data }) => { if (!data.requestId) { return } - await devtool.send({ - method: 'Network.webSocketClosed', - params: { - requestId: data.requestId, - timestamp: getTimestamp() - } - }) + try { + await devtool.send({ + method: 'Network.webSocketClosed', + params: { + requestId: data.requestId, + timestamp: devtool.getTimestamp() + } + }) + } finally { + networkPlugin.removeRequest(data.requestId) + } }) }) diff --git a/packages/network-debugger/src/fork/pipe/body-transformer.ts b/packages/network-debugger/src/fork/pipe/body-transformer.ts index f3525fe..d1d35b0 100644 --- a/packages/network-debugger/src/fork/pipe/body-transformer.ts +++ b/packages/network-debugger/src/fork/pipe/body-transformer.ts @@ -15,28 +15,30 @@ export class BodyTransformer { const match = contentType.match(/charset=([^;]+)/) const encoding = match ? match[1] : 'utf-8' - const isBinary = !/text|json|xml/.test(contentType) - const body = (() => { - if (req.responseData === undefined || req.responseData === null) { - return void 0 - } - if (isBinary) { - return req.responseData.toString('base64') - } - if (Buffer.isBuffer(req.responseData)) { - return iconv.decode(req.responseData, encoding) - } - // if responseData is `JSON.stringify(Buffer)` => {"type":"Buffer","data":[1,2,3,4,5]} - // need to decode the Buffer + const isBinary = !/text|json|xml|javascript|x-www-form-urlencoded/.test(contentType) + const responseBuffer = (() => { + if (Buffer.isBuffer(req.responseData)) return req.responseData if ( typeof req.responseData === 'object' && + req.responseData !== null && 'type' in req.responseData && req.responseData.type === 'Buffer' && 'data' in req.responseData && Array.isArray(req.responseData.data) ) { - return iconv.decode(Buffer.from(req.responseData.data), encoding) + return Buffer.from(req.responseData.data) + } + if (req.responseData instanceof Uint8Array) return Buffer.from(req.responseData) + return undefined + })() + const body = (() => { + if (req.responseData === undefined || req.responseData === null) { + return void 0 + } + if (isBinary) { + return (responseBuffer ?? Buffer.from(String(req.responseData))).toString('base64') } + if (responseBuffer) return iconv.decode(responseBuffer, encoding) return req.responseData })() diff --git a/packages/network-debugger/src/fork/request-center.test.ts b/packages/network-debugger/src/fork/request-center.test.ts index c39161e..e696b12 100644 --- a/packages/network-debugger/src/fork/request-center.test.ts +++ b/packages/network-debugger/src/fork/request-center.test.ts @@ -1,554 +1,111 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import { EventEmitter } from 'events' -import { READY_MESSAGE } from '../common' -import type { PluginInstance } from './module/common' - -// 定义 DevtoolServer 实例的类型 -interface MockDevtoolServerInstance { - listeners: Array< - ( - error: unknown | null, - message?: { method: string; params?: Record; id?: string } - ) => void - > - getTimestamp: () => number - timestamp: number -} - -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { - mockWsServerClose, - mockDevtoolOn, - mockDevtoolSend, - mockDevtoolClose, - mockLog, - mockProcessSend, - wsServerInstances, - devtoolInstances, - onConnectCallbacks -} = vi.hoisted(() => { - // 使用 unknown[] 来避免类型问题,在测试中使用时再转换 - const wsInstances: unknown[] = [] - const devInstances: unknown[] = [] - - return { - mockWsServerClose: vi.fn(), - mockDevtoolOn: vi.fn(), - mockDevtoolSend: vi.fn().mockResolvedValue(undefined), - mockDevtoolClose: vi.fn(), - mockLog: vi.fn(), - mockProcessSend: vi.fn(), - wsServerInstances: wsInstances, - devtoolInstances: devInstances, - onConnectCallbacks: [] as Array<() => void> - } +import { once } from 'node:events' +import { createServer } from 'node:http' +import type { AddressInfo } from 'node:net' +import { afterEach, describe, expect, test, vi } from 'vitest' +import { WebSocket } from 'ws' +import { RequestDetail } from '../common' +import { createPlugin, useHandler } from './module/common' +import { RequestCenter } from './request-center' + +const centers = new Set() +const sockets = new Set() + +afterEach(async () => { + for (const socket of sockets) socket.terminate() + sockets.clear() + await Promise.all([...centers].map((center) => center.close())) + centers.clear() }) -// 类型安全的辅助函数 -function getWsServer(index: number): EventEmitter { - const instance = wsServerInstances[index] - if (instance instanceof EventEmitter) { - return instance - } - // 对于 mock 创建的实例,它们继承自 EventEmitter 但 TypeScript 不知道 - // 我们通过检查 emit 方法来验证 - const emitter = instance as { emit?: (event: string, ...args: unknown[]) => boolean } - if (emitter && typeof emitter.emit === 'function') { - return instance as EventEmitter - } - throw new Error('Invalid WebSocket server instance') +async function createCenter(options: ConstructorParameters[0] = {}) { + const center = new RequestCenter({ serverPort: 0, ...options }) + centers.add(center) + const target = await center.ready + return { center, target } } -function getDevtool(index: number): MockDevtoolServerInstance { - const instance = devtoolInstances[index] - const devtool = instance as { listeners?: unknown[] } - if (devtool && Array.isArray(devtool.listeners)) { - return instance as MockDevtoolServerInstance - } - throw new Error('Invalid DevtoolServer instance') +async function connect(url: string) { + const socket = new WebSocket(url) + sockets.add(socket) + await once(socket, 'open') + return socket } -// Mock ws 模块 -vi.mock('ws', () => { - const events = require('events') - - return { - Server: class MockServer extends events.EventEmitter { - close = mockWsServerClose - constructor() { - super() - wsServerInstances.push(this) - } - } - } -}) - -// Mock DevtoolServer -vi.mock('./devtool', () => { - return { - DevtoolServer: class MockDevtoolServer { - listeners: Array< - ( - error: unknown | null, - message?: { method: string; params?: Record; id?: string } - ) => void - > = [] - timestamp = 0 - - constructor(options: { onConnect?: () => void }) { - devtoolInstances.push(this) - // 保存 onConnect 回调以便测试 - if (options.onConnect) { - onConnectCallbacks.push(options.onConnect) - } - } - - on( - listener: ( - error: unknown | null, - message?: { method: string; params?: Record; id?: string } - ) => void - ) { - mockDevtoolOn(listener) - this.listeners.push(listener) - } - - send(message: unknown) { - return mockDevtoolSend(message) - } - - close() { - mockDevtoolClose() - } - - getTimestamp() { - return this.timestamp - } - } - } -}) - -// Mock utils 模块 -vi.mock('../utils', () => ({ - log: mockLog -})) +function nextJson(socket: WebSocket): Promise> { + return once(socket, 'message').then(([raw]) => JSON.parse(raw.toString())) +} -describe('fork/request-center.ts', () => { - const originalProcessSend = process.send +describe('RequestCenter', () => { + test('exposes its bound target and does not bind the deprecated application port', async () => { + const occupied = createServer() + occupied.listen(0, '127.0.0.1') + await once(occupied, 'listening') + const occupiedPort = (occupied.address() as AddressInfo).port - beforeEach(() => { - vi.clearAllMocks() - wsServerInstances.length = 0 - devtoolInstances.length = 0 - onConnectCallbacks.length = 0 - // 设置 process.send - process.send = mockProcessSend - }) + const { center, target } = await createCenter({ port: occupiedPort }) + expect(center.target).toEqual(target) + expect(target.discoveryUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) - afterEach(() => { - // 恢复 process.send - process.send = originalProcessSend + await new Promise((resolve) => occupied.close(() => resolve())) }) - describe('RequestCenter 类', () => { - describe('构造函数', () => { - test('创建 DevtoolServer 和 WebSocket Server', async () => { - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - // 验证 DevtoolServer 被创建 - expect(devtoolInstances.length).toBe(1) - - // 验证 WebSocket Server 被创建 - expect(wsServerInstances.length).toBe(1) - }) - - test('使用正确的端口配置', async () => { - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 6000, - serverPort: 6001 - }) - - // 验证 DevtoolServer 的 on 方法被调用 - expect(mockDevtoolOn).toHaveBeenCalled() - }) - - test('DevtoolServer 连接时触发 onConnect 监听器', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const onConnectListener = vi.fn() - center.on('onConnect', onConnectListener) - - // 触发 DevtoolServer 的 onConnect 回调 - // 这个回调是在构造函数中传递给 DevtoolServer 的 - expect(onConnectCallbacks.length).toBe(1) - onConnectCallbacks[0]() - - expect(onConnectListener).toHaveBeenCalledWith({ - data: null, - id: 'onConnect' - }) - }) - - test('DevtoolServer 连接时没有监听器不会报错', async () => { - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - // 触发 DevtoolServer 的 onConnect 回调,但没有注册监听器 - expect(onConnectCallbacks.length).toBe(1) - // 不应该抛出错误 - expect(() => onConnectCallbacks[0]()).not.toThrow() - }) - }) - - describe('on 方法', () => { - test('注册消息监听器', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener = vi.fn() - center.on('test-method', listener) - - // 模拟 DevtoolServer 发送消息 - const devtool = getDevtool(0) - devtool.listeners.forEach((l) => - l(null, { method: 'test-method', params: { foo: 'bar' }, id: '123' }) - ) - - expect(listener).toHaveBeenCalledWith({ - data: { foo: 'bar' }, - id: '123' - }) - }) - - test('返回取消订阅函数', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener = vi.fn() - const unsubscribe = center.on('test-method', listener) - - // 取消订阅 - unsubscribe() - - // 模拟 DevtoolServer 发送消息 - const devtool = getDevtool(0) - devtool.listeners.forEach((l) => l(null, { method: 'test-method', params: { foo: 'bar' } })) - - // 监听器不应该被调用 - expect(listener).not.toHaveBeenCalled() - }) - - test('支持多个监听器订阅同一方法', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener1 = vi.fn() - const listener2 = vi.fn() - - center.on('test-method', listener1) - center.on('test-method', listener2) - - // 模拟 DevtoolServer 发送消息 - const devtool = getDevtool(0) - devtool.listeners.forEach((l) => l(null, { method: 'test-method', params: { foo: 'bar' } })) - - expect(listener1).toHaveBeenCalled() - expect(listener2).toHaveBeenCalled() - }) - - test('未注册的方法不触发监听器', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener = vi.fn() - center.on('test-method', listener) - - // 模拟 DevtoolServer 发送不同方法的消息 - const devtool = getDevtool(0) - devtool.listeners.forEach((l) => - l(null, { method: 'other-method', params: { foo: 'bar' } }) - ) - - expect(listener).not.toHaveBeenCalled() - }) - }) - - describe('DevtoolServer 错误处理', () => { - test('DevtoolServer 错误时记录日志', async () => { - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 5270, - serverPort: 5271 - }) + test('routes structured capture events directly to plugin listeners', async () => { + const { center } = await createCenter() + const listener = vi.fn() + center.on('initRequest', listener) + const request = new RequestDetail() + request.url = 'https://example.test/capture' - // 模拟 DevtoolServer 发送错误 - const devtool = getDevtool(0) - const error = new Error('Test error') - devtool.listeners.forEach((l) => l(error)) - - expect(mockLog).toHaveBeenCalledWith(error) - }) - }) - - describe('loadPlugins 方法', () => { - test('加载插件并执行', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const pluginFn = vi.fn().mockReturnValue({ result: 'test' }) - const plugin: PluginInstance<{ result: string }> = Object.assign(pluginFn, { - id: 'test-plugin' - }) - - center.loadPlugins([plugin]) - - expect(pluginFn).toHaveBeenCalledWith({ - devtool: devtoolInstances[0], - core: center, - plugins: [plugin] - }) - }) - - test('加载多个插件', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const plugin1Fn = vi.fn().mockReturnValue({ result: 'test1' }) - const plugin1: PluginInstance<{ result: string }> = Object.assign(plugin1Fn, { - id: 'plugin-1' - }) - - const plugin2Fn = vi.fn().mockReturnValue({ result: 'test2' }) - const plugin2: PluginInstance<{ result: string }> = Object.assign(plugin2Fn, { - id: 'plugin-2' - }) - - center.loadPlugins([plugin1, plugin2]) - - expect(plugin1Fn).toHaveBeenCalled() - expect(plugin2Fn).toHaveBeenCalled() - }) - }) - - describe('usePlugin 方法', () => { - test('获取已加载插件的输出', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const pluginOutput = { result: 'test', value: 42 } - const pluginFn = vi.fn().mockReturnValue(pluginOutput) - const plugin: PluginInstance = Object.assign(pluginFn, { - id: 'test-plugin' - }) - - center.loadPlugins([plugin]) - - const output = center.usePlugin('test-plugin') - expect(output).toEqual(pluginOutput) - }) - - test('获取不存在的插件返回 null', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const pluginFn = vi.fn().mockReturnValue({ result: 'test' }) - const plugin: PluginInstance<{ result: string }> = Object.assign(pluginFn, { - id: 'test-plugin' - }) - - center.loadPlugins([plugin]) - - const output = center.usePlugin('non-existent-plugin') - expect(output).toBeUndefined() - }) - - test('未加载插件时返回 null', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) + center.handleCaptureEvent({ type: 'initRequest', data: request }) + await vi.waitFor(() => expect(listener).toHaveBeenCalledOnce()) + expect(listener).toHaveBeenCalledWith({ data: request }) + }) - const output = center.usePlugin('any-plugin') - expect(output).toBeNull() - }) + test('supplies source-client result/error helpers to command plugins, including id 0', async () => { + const { center, target } = await createCenter() + const handler = vi.fn(async ({ id, client, result }) => { + expect(id).toBe(0) + expect(client).toBeInstanceOf(WebSocket) + await result?.({ echoed: true }) }) + center.on('Test.echo', handler) + const socket = await connect(target.webSocketDebuggerUrl) - describe('close 方法', () => { - test('关闭 WebSocket Server 和 DevtoolServer', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - center.close() + const response = nextJson(socket) + socket.send(JSON.stringify({ id: 0, method: 'Test.echo', params: { value: 1 } })) + await expect(response).resolves.toEqual({ id: 0, result: { echoed: true } }) + expect(handler).toHaveBeenCalledOnce() + }) - expect(mockWsServerClose).toHaveBeenCalled() - expect(mockDevtoolClose).toHaveBeenCalled() - }) + test('loads plugins, exposes outputs and invokes cleanup during idempotent close', async () => { + const { center } = await createCenter() + const cleanup = vi.fn() + const plugin = createPlugin('owned-plugin', () => { + useHandler('capture-owned', () => undefined) + return cleanup }) - describe('WebSocket Server 消息处理', () => { - test('处理来自主进程的消息', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener = vi.fn() - center.on('initRequest', listener) - - // 模拟 WebSocket 连接 - const wsServer = getWsServer(0) - const mockWsClient = new EventEmitter() - - wsServer.emit('connection', mockWsClient) - - // 模拟接收消息 - const message = JSON.stringify({ - type: 'initRequest', - data: { id: 'test-id', url: 'http://example.com' } - }) - mockWsClient.emit('message', Buffer.from(message)) - - expect(listener).toHaveBeenCalledWith({ - data: { id: 'test-id', url: 'http://example.com' } - }) - }) - - test('未知消息类型不触发监听器', async () => { - const { RequestCenter } = await import('./request-center') - - const center = new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - const listener = vi.fn() - center.on('initRequest', listener) - - // 模拟 WebSocket 连接 - const wsServer = getWsServer(0) - const mockWsClient = new EventEmitter() - - wsServer.emit('connection', mockWsClient) - - // 模拟接收未知类型的消息 - const message = JSON.stringify({ - type: 'unknownType', - data: { foo: 'bar' } - }) - mockWsClient.emit('message', Buffer.from(message)) - - expect(listener).not.toHaveBeenCalled() - }) - - test('WebSocket Server 监听时发送 READY_MESSAGE', async () => { - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - // 模拟 WebSocket Server 开始监听 - const wsServer = getWsServer(0) - wsServer.emit('listening') - - expect(mockProcessSend).toHaveBeenCalledWith(READY_MESSAGE) - }) - - test('process.send 不存在时不发送 READY_MESSAGE', async () => { - // 移除 process.send - const originalSend = process.send - process.send = undefined - - const { RequestCenter } = await import('./request-center') - - new RequestCenter({ - port: 5270, - serverPort: 5271 - }) - - // 模拟 WebSocket Server 开始监听 - const wsServer = getWsServer(0) - wsServer.emit('listening') - - // 不应该抛出错误 - expect(mockProcessSend).not.toHaveBeenCalled() - - // 恢复 process.send - process.send = originalSend - }) - }) + center.loadPlugins([plugin]) + expect(center.usePlugin('owned-plugin')).toBe(cleanup) + const firstClose = center.close() + const secondClose = center.close() + expect(secondClose).toBe(firstClose) + await firstClose + expect(cleanup).toHaveBeenCalledOnce() + }) - describe('默认端口配置', () => { - test('使用默认端口 PORT 当 port 为 0', async () => { - const { RequestCenter } = await import('./request-center') + test('notifies onConnect listeners for every reconnect', async () => { + const { center, target } = await createCenter() + const listener = vi.fn() + center.on('onConnect', listener) - // 当 port 为 0 时,应该使用默认的 PORT 常量 - new RequestCenter({ - port: 0, - serverPort: 5271 - }) + const first = await connect(target.webSocketDebuggerUrl) + await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(1)) + first.close() + await once(first, 'close') + sockets.delete(first) - // 验证 WebSocket Server 被创建 - expect(wsServerInstances.length).toBe(1) - }) - }) + await connect(target.webSocketDebuggerUrl) + await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2)) }) }) diff --git a/packages/network-debugger/src/fork/request-center.ts b/packages/network-debugger/src/fork/request-center.ts index d0389cd..d9db658 100644 --- a/packages/network-debugger/src/fork/request-center.ts +++ b/packages/network-debugger/src/fork/request-center.ts @@ -1,138 +1,176 @@ -import { DevtoolServer } from './devtool' -import { PORT, READY_MESSAGE, RequestDetail } from '../common' -import { Server } from 'ws' +import type { WebSocket } from 'ws' +import type { DevtoolsTarget } from '../adapters/types' +import type { RequestDetail } from '../common' +import type { CdpId, LegacyCaptureEvent } from '../legacy-bridge/contracts' import { log } from '../utils' -import { PluginInstance } from './module/common' +import { + DevtoolServer, + type DevtoolCommandContext, + type DevtoolMessage, + type DevtoolMessageRequest +} from './devtool' +import type { PluginInstance } from './module/common' export interface RequestCenterInitOptions { - port: number - serverPort: number + /** @deprecated The application-side WebSocket transport has been removed. */ + port?: number + /** CDP discovery/target port. `0` selects a free loopback port. */ + serverPort?: number + /** Stable identity used to preserve the target URL across child restarts. */ + targetId?: string + title?: string autoOpenDevtool?: boolean requests?: Record } -/** - * @param data message data - * @param id? message id, Only for devtool message - * @param request? request detail - */ -export interface DevtoolMessageListener { - (props: { data: T; request?: RequestDetail; id?: string }): void +export interface DevtoolMessageListenerProps { + data: T + request?: RequestDetail + id?: CdpId + /** Present for frontend commands, absent for capture events. */ + client?: WebSocket + /** Present for frontend commands. First response wins. */ + reply?: DevtoolCommandContext['reply'] + /** Present for frontend commands. */ + result?: DevtoolCommandContext['result'] + /** Present for frontend commands. */ + error?: DevtoolCommandContext['error'] +} + +export interface DevtoolMessageListener { + (props: DevtoolMessageListenerProps): unknown | Promise } +/** + * Routes in-process Legacy capture events to plugins and exposes one standard + * CDP discovery/target server. There is no application-side TCP transport. + */ export class RequestCenter { - private devtool: DevtoolServer - private server: Server - private listeners: Record | undefined> = {} - private options: RequestCenterInitOptions - constructor(options: RequestCenterInitOptions) { - this.options = options - const { serverPort, autoOpenDevtool } = options + public readonly ready: Promise + private readonly devtool: DevtoolServer + private readonly listeners: Record | undefined> = {} + private plugins: PluginInstance[] = [] + private pluginOutputs?: Map + private closePromise?: Promise + private captureQueue: Promise = Promise.resolve() + + constructor(options: RequestCenterInitOptions = {}) { this.devtool = new DevtoolServer({ - port: serverPort, - autoOpenDevtool: autoOpenDevtool, + port: options.serverPort ?? 0, + targetId: options.targetId, + title: options.title, + autoOpenDevtool: options.autoOpenDevtool, onConnect: () => { - const listeners = this.listeners['onConnect'] - listeners?.forEach((listener) => - listener({ - data: null, - id: 'onConnect' - }) - ) + void this.dispatch('onConnect', { + data: null, + id: 'onConnect' + }).catch(log) } }) - this.devtool.on((error, message) => { + this.ready = this.devtool.ready + + this.devtool.on(async (error, message, context) => { if (error) { log(error) - return + return false } - - const listenerList = this.listeners[message.method] - if (!listenerList) { - return + if (!message || !context || !('method' in message) || typeof message.method !== 'string') { + return false } - - listenerList.forEach((listener) => { - listener({ - data: message.params, - id: message.id - }) - }) + const request = message as DevtoolMessageRequest + return this.dispatchCommand(request.method, request.params ?? {}, request.id, context) }) - this.server = this.initServer() } - #plugins: PluginInstance[] = [] - #pluginOutputs?: Map + get target(): DevtoolsTarget | undefined { + return this.devtool.target + } + public loadPlugins(plugins: PluginInstance[]) { - this.#plugins = plugins - this.#pluginOutputs = new Map() + this.plugins = plugins + this.pluginOutputs = new Map() - plugins.forEach((plugin) => { + for (const plugin of plugins) { const output = plugin({ devtool: this.devtool, core: this, - plugins: this.#plugins + plugins: this.plugins }) - this.#pluginOutputs!.set(plugin.id, output) - }) + this.pluginOutputs.set(plugin.id, output) + } } public usePlugin(id: string) { - if (!this.#pluginOutputs) { - return null as T - } - - const output = this.#pluginOutputs.get(id) - return output as T + if (!this.pluginOutputs) return null as T + return this.pluginOutputs.get(id) as T } - public on(method: string, listener: DevtoolMessageListener) { - if (!this.listeners[method]) { - this.listeners[method] = new Set() - } - this.listeners[method]!.add(listener) - return () => { - this.listeners[method]!.delete(listener) - } + public on(method: string, listener: DevtoolMessageListener) { + if (!this.listeners[method]) this.listeners[method] = new Set() + const listeners = this.listeners[method] as Set> + listeners.add(listener) + return () => listeners.delete(listener) } - public close() { - this.server.close() - this.devtool.close() + /** Deliver an IPC-decoded capture event without another serialization hop. */ + public handleCaptureEvent(event: LegacyCaptureEvent): void { + if (this.closePromise) return + this.captureQueue = this.captureQueue + .then(async () => { + await this.dispatch(event.type, { data: event.data }) + }) + .catch(log) } - private initServer() { - const server = new Server({ port: this.options.port || PORT }) - server.on('connection', (ws) => { - ws.on('message', (data) => { - const message = JSON.parse(data.toString()) - const _message = message as { type: string; data: any } - switch (_message.type) { - default: - { - const listenerList = this.listeners[_message.type] - if (!listenerList) { - // console.warn('unknown message type', _message.type) - break - } - - listenerList.forEach((listener) => { - listener({ - data: _message.data - }) - }) - } - break + public close(): Promise { + if (this.closePromise) return this.closePromise + this.closePromise = (async () => { + await this.captureQueue + if (this.pluginOutputs) { + for (const output of this.pluginOutputs.values()) { + if (typeof output === 'function') await output() } - }) - }) - server.on('listening', () => { - if (process.send) { - process.send(READY_MESSAGE) } - }) + for (const key of Object.keys(this.listeners)) this.listeners[key]?.clear() + await this.devtool.close() + })() + return this.closePromise + } + + private async dispatchCommand( + method: string, + params: Record, + id: CdpId | undefined, + context: DevtoolCommandContext + ): Promise { + if (id === undefined) return false + const listeners = this.listeners[method] + if (!listeners?.size) return false - return server + for (const listener of [...listeners]) { + await listener({ + data: params, + id, + client: context.client, + reply: context.reply, + result: context.result, + error: context.error + }) + } + return true + } + + private async dispatch( + method: string, + props: DevtoolMessageListenerProps + ): Promise { + const listeners = this.listeners[method] + if (!listeners?.size) return false + for (const listener of [...listeners]) await listener(props) + return true } } + +// Kept as a type-only compatibility export for plugins compiled against the +// previous RequestCenter module. +export type { DevtoolMessage } diff --git a/packages/network-debugger/src/fork/resource-service.test.ts b/packages/network-debugger/src/fork/resource-service.test.ts index 7769be3..819ae89 100644 --- a/packages/network-debugger/src/fork/resource-service.test.ts +++ b/packages/network-debugger/src/fork/resource-service.test.ts @@ -1,852 +1,165 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import { fileURLToPath, pathToFileURL } from 'url' -import path from 'path' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { ResourceService, ScriptMap } from './resource-service' + +let fixtureDir = '' + +function fixture(name: string, source = '') { + const filePath = path.join(fixtureDir, name) + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, source) + return filePath +} + +beforeEach(() => { + fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nnd-resource-')) +}) -// 使用 vi.hoisted 确保变量在 mock 提升时可用 -const { - mockReadFileSync, - mockReaddirSync, - mockStatSync, - mockOpenSync, - mockReadSync, - mockCloseSync, - mockExistsSync, - fileContents, - directoryStructure -} = vi.hoisted(() => { - return { - mockReadFileSync: vi.fn(), - mockReaddirSync: vi.fn(), - mockStatSync: vi.fn(), - mockOpenSync: vi.fn(), - mockReadSync: vi.fn(), - mockCloseSync: vi.fn(), - mockExistsSync: vi.fn(), - fileContents: new Map(), - directoryStructure: new Map() - } +afterEach(() => { + vi.restoreAllMocks() + fs.rmSync(fixtureDir, { recursive: true, force: true }) }) -// Mock fs 模块 -vi.mock('fs', () => { - return { - default: { - readFileSync: mockReadFileSync, - readdirSync: mockReaddirSync, - statSync: mockStatSync, - openSync: mockOpenSync, - readSync: mockReadSync, - closeSync: mockCloseSync, - existsSync: mockExistsSync - }, - readFileSync: mockReadFileSync, - readdirSync: mockReaddirSync, - statSync: mockStatSync, - openSync: mockOpenSync, - readSync: mockReadSync, - closeSync: mockCloseSync, - existsSync: mockExistsSync - } +describe('ScriptMap', () => { + test('maintains bidirectional mappings and replaces a URL mapping', () => { + const map = new ScriptMap() + map.addMapping('file:///one.js', '1') + map.addMapping('file:///two.js', '2') + map.addMapping('file:///one.js', '3') + + expect(map.getScriptIdByUrl('file:///one.js')).toBe('3') + expect(map.getScriptIdByUrl('file:///two.js')).toBe('2') + expect(map.getUrlByScriptId('1')).toBe('file:///one.js') + expect(map.getUrlByScriptId('3')).toBe('file:///one.js') + expect(map.getScriptIdByUrl('file:///missing.js')).toBeUndefined() + }) }) -// Mock common 模块中的 __dirname -vi.mock('../common', () => ({ - __dirname: '/mock/common/dir' -})) +describe('ResourceService lazy registration', () => { + test('starts empty and never scans cwd eagerly', () => { + const unreferenced = fixture('unreferenced.js', 'console.log("not announced")') + const service = new ResourceService() -describe('fork/resource-service.ts', () => { - beforeEach(() => { - vi.clearAllMocks() - fileContents.clear() - directoryStructure.clear() + expect(service.getLocalScriptList()).toEqual([]) + expect(service.getUrlByScriptId('1')).toBeUndefined() + expect(fs.existsSync(unreferenced)).toBe(true) }) - afterEach(() => { - vi.restoreAllMocks() + test('registers an existing file only when its URL is requested', () => { + const filePath = fixture('app.js', 'export const value = 1') + const fileUrl = pathToFileURL(filePath).href + const service = new ResourceService() + + const id = service.getScriptIdByUrl(fileUrl) + + expect(id).toBe('1') + expect(service.getScriptIdByUrl(fileUrl)).toBe(id) + expect(service.getScriptIdByUrl(filePath)).toBe(id) + expect(service.getUrlByScriptId(id!)).toBe(fileUrl) + expect(service.getLocalScriptList()).toEqual([ + expect.objectContaining({ + scriptId: id, + url: fileUrl, + embedderName: fileUrl, + scriptLanguage: 'JavaScript' + }) + ]) }) - describe('ScriptMap 类', () => { - test('addMapping 添加 URL 和 scriptId 的双向映射', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() - - scriptMap.addMapping('file:///test/file.js', '1') - - expect(scriptMap.getScriptIdByUrl('file:///test/file.js')).toBe('1') - expect(scriptMap.getUrlByScriptId('1')).toBe('file:///test/file.js') - }) - - test('getUrlByScriptId 返回正确的 URL', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() - - scriptMap.addMapping('file:///path/to/script.js', '42') - - expect(scriptMap.getUrlByScriptId('42')).toBe('file:///path/to/script.js') - }) - - test('getUrlByScriptId 对于不存在的 scriptId 返回 undefined', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() - - expect(scriptMap.getUrlByScriptId('non-existent')).toBeUndefined() - }) - - test('getScriptIdByUrl 返回正确的 scriptId', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() + test('does not register missing files, directories, or invalid file URLs', () => { + const service = new ResourceService() - scriptMap.addMapping('file:///another/script.mjs', '100') - - expect(scriptMap.getScriptIdByUrl('file:///another/script.mjs')).toBe('100') - }) - - test('getScriptIdByUrl 对于不存在的 URL 返回 undefined', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() - - expect(scriptMap.getScriptIdByUrl('file:///non-existent.js')).toBeUndefined() - }) - - test('支持多个映射', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() - - scriptMap.addMapping('file:///file1.js', '1') - scriptMap.addMapping('file:///file2.js', '2') - scriptMap.addMapping('file:///file3.cjs', '3') - - expect(scriptMap.getScriptIdByUrl('file:///file1.js')).toBe('1') - expect(scriptMap.getScriptIdByUrl('file:///file2.js')).toBe('2') - expect(scriptMap.getScriptIdByUrl('file:///file3.cjs')).toBe('3') + expect(service.getScriptIdByUrl(path.join(fixtureDir, 'missing.js'))).toBeUndefined() + expect(service.getScriptIdByUrl(fixtureDir)).toBeUndefined() + expect(service.getScriptIdByUrl('file:///%zz')).toBeUndefined() + expect(service.getLocalScriptList()).toEqual([]) + }) - expect(scriptMap.getUrlByScriptId('1')).toBe('file:///file1.js') - expect(scriptMap.getUrlByScriptId('2')).toBe('file:///file2.js') - expect(scriptMap.getUrlByScriptId('3')).toBe('file:///file3.cjs') + test.each([ + ['entry.js', 'JavaScript'], + ['entry.mjs', 'JavaScript'], + ['entry.cjs', 'JavaScript'], + ['module.wasm', 'WebAssembly'], + ['entry.ts', 'Unknown'], + ['README', 'Unknown'] + ])('classifies %s as %s after lazy registration', (name, language) => { + const service = new ResourceService() + const filePath = fixture(name, 'content') + + const id = service.getScriptIdByUrl(filePath) + + expect(service.getLocalScriptList().find((script) => script.scriptId === id)).toMatchObject({ + scriptLanguage: language }) + }) - test('覆盖已存在的映射', async () => { - const { ScriptMap } = await import('./resource-service') - const scriptMap = new ScriptMap() + test('resolves an external source map relative to the script', () => { + const service = new ResourceService() + const mapPath = fixture('maps/app.js.map', '{}') + const scriptPath = fixture('maps/app.js', 'console.log(1)\n//# sourceMappingURL=app.js.map\n') - scriptMap.addMapping('file:///file.js', '1') - scriptMap.addMapping('file:///file.js', '2') + const id = service.getScriptIdByUrl(scriptPath) + const descriptor = service.getLocalScriptList().find((script) => script.scriptId === id) - // URL 映射到新的 scriptId - expect(scriptMap.getScriptIdByUrl('file:///file.js')).toBe('2') - // 旧的 scriptId 仍然映射到 URL - expect(scriptMap.getUrlByScriptId('1')).toBe('file:///file.js') - // 新的 scriptId 也映射到 URL - expect(scriptMap.getUrlByScriptId('2')).toBe('file:///file.js') + expect(descriptor).toMatchObject({ + sourceMapURL: pathToFileURL(mapPath).href, + hasSourceURL: true }) }) - describe('ResourceService 类', () => { - describe('getScriptIdByUrl 方法', () => { - test('返回正确的 scriptId', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - // 通过内部 scriptMap 添加映射(使用 traverseDirToMap 间接测试) - // 由于 scriptMap 是私有的,我们需要通过 getLocalScriptList 来添加映射 - // 或者直接测试返回 undefined 的情况 - expect(service.getScriptIdByUrl('file:///non-existent.js')).toBeUndefined() - }) - }) + test('preserves inline source maps and reports scripts without maps', () => { + const service = new ResourceService() + const inline = 'data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==' + const inlinePath = fixture('inline.js', `code();\n//# sourceMappingURL=${inline}`) + const plainPath = fixture('plain.js', 'code();') - describe('getUrlByScriptId 方法', () => { - test('返回正确的 URL', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() + const inlineId = service.getScriptIdByUrl(inlinePath) + const plainId = service.getScriptIdByUrl(plainPath) + const scripts = service.getLocalScriptList() - expect(service.getUrlByScriptId('non-existent')).toBeUndefined() - }) + expect(scripts.find((script) => script.scriptId === inlineId)).toMatchObject({ + sourceMapURL: inline, + hasSourceURL: true }) - - describe('getScriptSource 方法', () => { - test('返回脚本源代码', async () => { - const { ResourceService, ScriptMap } = await import('./resource-service') - const service = new ResourceService() - - // 手动设置映射(通过反射访问私有属性) - const scriptMap = Reflect.get(service, 'scriptMap') as InstanceType - const testUrl = pathToFileURL('/test/script.js').href - scriptMap.addMapping(testUrl, '1') - - // 设置 mock 返回值 - mockReadFileSync.mockReturnValue('console.log("hello");') - - const source = service.getScriptSource('1') - - expect(source).toBe('console.log("hello");') - expect(mockReadFileSync).toHaveBeenCalledWith('/test/script.js', 'utf-8') - }) - - test('scriptId 不存在时返回 null', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - const source = service.getScriptSource('non-existent') - - expect(source).toBeNull() - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'No file path found for script ID: non-existent' - ) - - consoleErrorSpy.mockRestore() - }) - - test('读取文件失败时返回 null', async () => { - const { ResourceService, ScriptMap } = await import('./resource-service') - const service = new ResourceService() - - // 手动设置映射 - const scriptMap = Reflect.get(service, 'scriptMap') as InstanceType - const testUrl = pathToFileURL('/test/error-script.js').href - scriptMap.addMapping(testUrl, '2') - - // 设置 mock 抛出错误 - const readError = new Error('File not found') - mockReadFileSync.mockImplementation(() => { - throw readError - }) - - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - const source = service.getScriptSource('2') - - expect(source).toBeNull() - expect(consoleErrorSpy).toHaveBeenCalledWith('Error reading file:', readError) - - consoleErrorSpy.mockRestore() - }) + expect(scripts.find((script) => script.scriptId === plainId)).toMatchObject({ + sourceMapURL: '', + hasSourceURL: false }) + }) - describe('readLastLine 方法', () => { - test('读取文件最后一行', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const fileContent = 'line1\nline2\nline3\nlast line' - const filePath = '/test/file.txt' - const fileSize = Buffer.byteLength(fileContent) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation( - (fd: number, buffer: Buffer, offset: number, length: number, position: number) => { - const content = fileContent.slice(position, position + length) - buffer.write(content, offset) - return content.length - } - ) - mockCloseSync.mockImplementation(() => {}) - - const stat = { size: fileSize } as import('fs').Stats - - const result = service.readLastLine(filePath, stat, 1) - - expect(mockOpenSync).toHaveBeenCalledWith(filePath, 'r') - expect(mockCloseSync).toHaveBeenCalledWith(1) - expect(result).toBe('last line') - }) - - test('读取文件最后两行', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const fileContent = 'line1\nline2\nline3\nlast line' - const filePath = '/test/file.txt' - const fileSize = Buffer.byteLength(fileContent) - - mockOpenSync.mockReturnValue(2) - mockReadSync.mockImplementation( - (fd: number, buffer: Buffer, offset: number, length: number, position: number) => { - const content = fileContent.slice(position, position + length) - buffer.write(content, offset) - return content.length - } - ) - mockCloseSync.mockImplementation(() => {}) - - const stat = { size: fileSize } as import('fs').Stats - - const result = service.readLastLine(filePath, stat, 2) - - expect(result).toContain('line3') - expect(result).toContain('last line') - }) - - test('文件小于 chunkSize 时正确处理', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const fileContent = 'small file' - const filePath = '/test/small.txt' - const fileSize = Buffer.byteLength(fileContent) - - mockOpenSync.mockReturnValue(3) - mockReadSync.mockImplementation( - (fd: number, buffer: Buffer, offset: number, length: number, position: number) => { - const content = fileContent.slice(position, position + length) - buffer.write(content, offset) - return content.length - } - ) - mockCloseSync.mockImplementation(() => {}) - - const stat = { size: fileSize } as import('fs').Stats - - const result = service.readLastLine(filePath, stat, 1) - - expect(result).toBe('small file') - }) - - test('默认读取 1 行', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const fileContent = 'line1\nline2' - const filePath = '/test/default.txt' - const fileSize = Buffer.byteLength(fileContent) - - mockOpenSync.mockReturnValue(4) - mockReadSync.mockImplementation( - (fd: number, buffer: Buffer, offset: number, length: number, position: number) => { - const content = fileContent.slice(position, position + length) - buffer.write(content, offset) - return content.length - } - ) - mockCloseSync.mockImplementation(() => {}) - - const stat = { size: fileSize } as import('fs').Stats - - // 不传 totalLines 参数,使用默认值 1 - const result = service.readLastLine(filePath, stat) - - expect(result).toBe('line2') - }) - - test('需要多次读取时 startPos 变为负数的情况', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - // 创建一个大于 1024 字节的文件,这样 chunkSize = 1024 - // startPos = fileSize - 1024 - // 当 startPos -= 1024 后,如果 startPos < 0,则进入分支 - // 例如:fileSize = 1500,chunkSize = 1024,startPos = 476 - // 第一次循环后:startPos = 476 - 1024 = -548 < 0,进入分支 - // 分支内:startPos = 0,buffer = Buffer.alloc(1500 - 0) = Buffer.alloc(1500) - // 然后循环继续(如果 lines.length < totalLines) - - // 创建一个 1500 字节左右的文件 - const lineContent = 'x'.repeat(100) // 每行 100 个字符 - const lines = [] - for (let i = 0; i < 15; i++) { - lines.push(`${lineContent}${i.toString().padStart(2, '0')}`) - } - const fileContent = lines.join('\n') - const filePath = '/test/multiread.txt' - const fileSize = Buffer.byteLength(fileContent) // 约 1545 字节 - - mockOpenSync.mockReturnValue(5) - mockReadSync.mockImplementation( - (fd: number, buffer: Buffer, offset: number, length: number, position: number) => { - const actualPosition = Math.max(0, position) - const endPosition = Math.min(actualPosition + length, fileSize) - const content = fileContent.slice(actualPosition, endPosition) - buffer.write(content, offset) - return content.length - } - ) - mockCloseSync.mockImplementation(() => {}) - - const stat = { size: fileSize } as import('fs').Stats - - // 请求读取 15 行,这会导致循环执行多次 - // 第一次读取后,可能只有部分行,需要继续读取 - const result = service.readLastLine(filePath, stat, 15) - - // 验证结果包含最后的行 - const resultLines = result.split('\n') - expect(resultLines.length).toBe(15) - }) - }) - - describe('getLocalScriptList 方法', () => { - test('遍历目录并返回脚本列表', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - // 保存原始的 process.cwd - const originalCwd = process.cwd - - // Mock process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - // 设置目录结构 - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['index.js', 'utils.mjs', 'lib'] - } - if (dirPath === '/mock/project/lib') { - return ['helper.cjs'] - } - if (dirPath === '/mock/common/dir') { - return ['core.js'] - } - return [] - }) - - mockStatSync.mockImplementation((filePath: string) => { - if (filePath === '/mock/project/lib' || filePath === path.join('/mock/project', 'lib')) { - return { isDirectory: () => true, size: 0 } - } - return { isDirectory: () => false, size: 100 } - }) - - // Mock readLastLine 相关 - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// no sourcemap') - return 15 - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - // 验证返回的脚本列表 - expect(scripts.length).toBeGreaterThan(0) - - // 验证脚本属性 - const jsScript = scripts.find((s) => s.url.includes('index.js')) - if (jsScript) { - expect(jsScript.scriptLanguage).toBe('JavaScript') - expect(jsScript.scriptId).toBeDefined() - } - - // 恢复 process.cwd - process.cwd = originalCwd - }) - - test('忽略 node_modules 目录', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['index.js', 'node_modules'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - // node_modules 不应该被访问 - if (dirPath.includes('node_modules')) { - throw new Error('Should not access node_modules') - } - return [] - }) - - mockStatSync.mockImplementation((filePath: string) => { - if (filePath.includes('node_modules')) { - return { isDirectory: () => true, size: 0 } - } - return { isDirectory: () => false, size: 100 } - }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// code') - return 7 - }) - mockCloseSync.mockImplementation(() => {}) - - // 不应该抛出错误 - expect(() => service.getLocalScriptList()).not.toThrow() - - process.cwd = originalCwd - }) - - test('正确识别 JavaScript 文件扩展名', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['file.js', 'file.mjs', 'file.cjs'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// code') - return 7 - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - const jsFiles = scripts.filter((s) => s.scriptLanguage === 'JavaScript') - expect(jsFiles.length).toBe(3) - - process.cwd = originalCwd - }) - - test('正确识别 WebAssembly 文件', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['module.wasm'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 1000 }) - - const scripts = service.getLocalScriptList() - - const wasmFile = scripts.find((s) => s.url.includes('.wasm')) - expect(wasmFile).toBeDefined() - expect(wasmFile?.scriptLanguage).toBe('WebAssembly') - - process.cwd = originalCwd - }) - - test('未知扩展名返回 Unknown', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['file.txt', 'file.json', 'file'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) - - const scripts = service.getLocalScriptList() - - const unknownFiles = scripts.filter((s) => s.scriptLanguage === 'Unknown') - expect(unknownFiles.length).toBe(3) - - process.cwd = originalCwd - }) - - test('文件名没有扩展名时返回 Unknown', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['Makefile', 'Dockerfile', 'README'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) - - const scripts = service.getLocalScriptList() - - // 所有没有扩展名的文件都应该返回 Unknown - scripts.forEach((script) => { - expect(script.scriptLanguage).toBe('Unknown') - }) - - process.cwd = originalCwd - }) - - test('处理带有 sourceMappingURL 的文件', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['bundle.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 200 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - const content = '// some code\n//# sourceMappingURL=bundle.js.map' - buffer.write(content) - return content.length - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - const bundleScript = scripts.find((s) => s.url.includes('bundle.js')) - expect(bundleScript).toBeDefined() - expect(bundleScript?.hasSourceURL).toBe(true) - expect(bundleScript?.sourceMapURL).toContain('bundle.js.map') - - process.cwd = originalCwd - }) - - test('处理内联 sourcemap (data: URL)', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['inline.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 500 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - const content = - '// code\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==' - buffer.write(content) - return content.length - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - const inlineScript = scripts.find((s) => s.url.includes('inline.js')) - expect(inlineScript).toBeDefined() - expect(inlineScript?.hasSourceURL).toBe(true) - expect(inlineScript?.sourceMapURL).toContain('data:application/json') - - process.cwd = originalCwd - }) - - test('处理 TypeScript 文件的 sourcemap', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['app.ts'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 300 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - const content = '// ts code\n//# sourceMappingURL=app.ts.map' - buffer.write(content) - return content.length - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - const tsScript = scripts.find((s) => s.url.includes('app.ts')) - expect(tsScript).toBeDefined() - expect(tsScript?.hasSourceURL).toBe(true) - - process.cwd = originalCwd - }) - - test('没有 sourcemap 的文件', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['simple.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 100 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - const content = 'console.log("hello");' - buffer.write(content) - return content.length - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - const simpleScript = scripts.find((s) => s.url.includes('simple.js')) - expect(simpleScript).toBeDefined() - expect(simpleScript?.hasSourceURL).toBe(false) - expect(simpleScript?.sourceMapURL).toBe('') - - process.cwd = originalCwd - }) - - test('scriptId 递增', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['a.js', 'b.js', 'c.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// code') - return 7 - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - // 验证 scriptId 是递增的 - const scriptIds = scripts.map((s) => parseInt(s.scriptId, 10)) - for (let i = 1; i < scriptIds.length; i++) { - expect(scriptIds[i]).toBeGreaterThan(scriptIds[i - 1]) - } - - process.cwd = originalCwd - }) - - test('embedderName 与 url 相同', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['test.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) - - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) - - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// code') - return 7 - }) - mockCloseSync.mockImplementation(() => {}) - - const scripts = service.getLocalScriptList() - - scripts.forEach((script) => { - expect(script.embedderName).toBe(script.url) - }) - - process.cwd = originalCwd - }) - - test('调用 getLocalScriptList 后可以通过 getScriptIdByUrl 获取 scriptId', async () => { - const { ResourceService } = await import('./resource-service') - const service = new ResourceService() - - const originalCwd = process.cwd - process.cwd = vi.fn().mockReturnValue('/mock/project') - - mockReaddirSync.mockImplementation((dirPath: string) => { - if (dirPath === '/mock/project') { - return ['mapped.js'] - } - if (dirPath === '/mock/common/dir') { - return [] - } - return [] - }) + test('returns source only for a lazily registered script', () => { + const source = 'export default "source"\n' + const filePath = fixture('source.js', source) + const service = new ResourceService() + const error = vi.spyOn(console, 'error').mockImplementation(() => undefined) - mockStatSync.mockReturnValue({ isDirectory: () => false, size: 50 }) + expect(service.getScriptSource('missing')).toBeNull() + const id = service.getScriptIdByUrl(filePath) + expect(service.getScriptSource(id!)).toBe(source) + expect(error).toHaveBeenCalledWith('No file path found for script ID: missing') + }) - mockOpenSync.mockReturnValue(1) - mockReadSync.mockImplementation((fd: number, buffer: Buffer) => { - buffer.write('// code') - return 7 - }) - mockCloseSync.mockImplementation(() => {}) + test('allocates stable increasing IDs without duplicating aliases', () => { + const first = fixture('first.js', '') + const second = fixture('second.js', '') + const service = new ResourceService() - const scripts = service.getLocalScriptList() + expect(service.getScriptIdByUrl(first)).toBe('1') + expect(service.getScriptIdByUrl(pathToFileURL(first).href)).toBe('1') + expect(service.getScriptIdByUrl(second)).toBe('2') + expect(service.getLocalScriptList()).toHaveLength(2) + }) - // 验证映射已建立 - const script = scripts[0] - if (script) { - expect(service.getScriptIdByUrl(script.url)).toBe(script.scriptId) - expect(service.getUrlByScriptId(script.scriptId)).toBe(script.url) - } + test('reads bounded final lines with and without a trailing newline', () => { + const filePath = fixture('tail.js', 'first\nsecond\nthird') + const service = new ResourceService() + const stat = fs.statSync(filePath) - process.cwd = originalCwd - }) - }) + expect(service.readLastLine(filePath, stat)).toBe('third') + expect(service.readLastLine(filePath, stat, 2)).toBe('second\nthird') + expect(service.readLastLine(filePath, stat, 0)).toBe('') }) }) diff --git a/packages/network-debugger/src/fork/resource-service.ts b/packages/network-debugger/src/fork/resource-service.ts index 553d85e..bc71d15 100644 --- a/packages/network-debugger/src/fork/resource-service.ts +++ b/packages/network-debugger/src/fork/resource-service.ts @@ -1,7 +1,15 @@ import fs from 'fs' import path from 'path' import { fileURLToPath, pathToFileURL } from 'url' -import { __dirname } from '../common' + +export interface ScriptDescriptor { + url: string + scriptLanguage: string + embedderName: string + scriptId: string + sourceMapURL: string + hasSourceURL: boolean +} // Actually Allowed Values: JavaScript, WebAssembly function getScriptLangByFileName(fileName: string) { @@ -32,6 +40,11 @@ export class ScriptMap { this.scriptIdToUrl.set(scriptId, filePath) } + /** Add an alternate lookup key without replacing the canonical file URL. */ + public addAlias(alias: string, scriptId: string) { + this.urlToScriptId.set(alias, scriptId) + } + public getUrlByScriptId(scriptId: string) { return this.scriptIdToUrl.get(scriptId) } @@ -44,14 +57,18 @@ export class ScriptMap { export class ResourceService { private scriptMap: ScriptMap private scriptIdCounter: number + private scripts: Map constructor() { this.scriptMap = new ScriptMap() this.scriptIdCounter = 0 + this.scripts = new Map() } public getScriptIdByUrl(url: string) { - return this.scriptMap.getScriptIdByUrl(url) + const existing = this.scriptMap.getScriptIdByUrl(url) + if (existing) return existing + return this.registerScript(url)?.scriptId } public getUrlByScriptId(scriptId: string) { @@ -82,86 +99,67 @@ export class ResourceService { * @returns string */ readLastLine(filePath: string, stat: fs.Stats, totalLines = 1) { - const fileSize = stat.size - const chunkSize = Math.min(1024, fileSize) - let startPos = fileSize - chunkSize - let buffer = Buffer.alloc(chunkSize) - let lines: string[] = [] - + if (totalLines <= 0 || stat.size === 0) return '' + // Source-map directives are short and live at EOF. A bounded tail read + // avoids the old zero-progress loop for files without a trailing newline. + const length = Math.min(stat.size, 64 * 1024) + const start = stat.size - length + const buffer = Buffer.alloc(length) const fd = fs.openSync(filePath, 'r') - - while (lines.length < totalLines && startPos >= 0) { - fs.readSync(fd, buffer, 0, chunkSize, startPos) - const chunk = buffer.toString('utf8') - lines = chunk.split('\n').concat(lines) - startPos -= chunkSize - if (startPos < 0) { - startPos = 0 - buffer = Buffer.alloc(fileSize - startPos) - } + try { + fs.readSync(fd, buffer, 0, length, start) + } finally { + fs.closeSync(fd) } + return buffer.toString('utf8').split(/\r?\n/).slice(-totalLines).join('\n') + } - fs.closeSync(fd) - - // Return the last `totalLines` lines - return lines.slice(-totalLines).join('\n') + public getLocalScriptList() { + return [...this.scripts.values()] } - private traverseDirToMap(directoryPath: string, ignoreList: string[] = ['node_modules']) { - const scriptList = [] - const stack = [directoryPath] - let scriptId = this.scriptIdCounter - - while (stack.length > 0) { - const currentPath = stack.pop()! - const items = fs.readdirSync(currentPath) - for (const item of items) { - if (ignoreList.includes(item)) { - continue - } - - const fullPath = path.join(currentPath, item) - const stats = fs.statSync(fullPath) - if (stats.isDirectory()) { - stack.push(fullPath) - } else { - const resolvedPath = path.resolve(fullPath) - const fileUrl = pathToFileURL(resolvedPath) - const scriptIdStr = `${++scriptId}` - let sourceMapURL = '' - if (/\.(js|ts)$/.test(resolvedPath)) { - const lastChunkCode = this.readLastLine(fullPath, stats, 2) - const sourceMapFilePathMatch = lastChunkCode.match(/sourceMappingURL=(.+)$/m)?.[1] ?? '' - sourceMapURL = sourceMapFilePathMatch - ? sourceMapFilePathMatch.startsWith('data:') - ? // inline sourcemap - sourceMapFilePathMatch - : // file path - pathToFileURL(path.join(currentPath, sourceMapFilePathMatch)).href - : '' - } - const url = fileUrl.href - const scriptLanguage = getScriptLangByFileName(url) - scriptList.push({ - url, - scriptLanguage, - embedderName: fileUrl.href, - scriptId: scriptIdStr, - sourceMapURL: sourceMapURL, - hasSourceURL: Boolean(sourceMapURL) - }) - this.scriptMap.addMapping(url, scriptIdStr) - } + private registerScript(input: string): ScriptDescriptor | undefined { + let filePath: string + try { + filePath = input.startsWith('file:') ? fileURLToPath(input) : path.resolve(input) + } catch { + return undefined + } + let stat: fs.Stats + try { + stat = fs.statSync(filePath) + } catch { + return undefined + } + if (!stat || typeof stat.isFile !== 'function' || !stat.isFile()) return undefined + + const url = pathToFileURL(filePath).href + const known = this.scriptMap.getScriptIdByUrl(url) + if (known) return this.scripts.get(known) + + let sourceMapURL = '' + if (/\.(?:[cm]?js|ts)$/i.test(filePath)) { + const tail = this.readLastLine(filePath, stat, 2) + const sourceMap = tail.match(/sourceMappingURL=(.+)$/m)?.[1]?.trim() + if (sourceMap) { + sourceMapURL = sourceMap.startsWith('data:') + ? sourceMap + : pathToFileURL(path.resolve(path.dirname(filePath), sourceMap)).href } } - this.scriptIdCounter += scriptList.length - return scriptList - } - - public getLocalScriptList() { - const projectScripts = this.traverseDirToMap(process.cwd()) - const coreScripts = this.traverseDirToMap(__dirname) - return [...projectScripts, ...coreScripts] + const scriptId = `${++this.scriptIdCounter}` + const descriptor: ScriptDescriptor = { + url, + scriptLanguage: getScriptLangByFileName(url), + embedderName: url, + scriptId, + sourceMapURL, + hasSourceURL: Boolean(sourceMapURL) + } + this.scriptMap.addMapping(url, scriptId) + if (input !== url) this.scriptMap.addAlias(input, scriptId) + this.scripts.set(scriptId, descriptor) + return descriptor } } diff --git a/packages/network-debugger/src/legacy-bridge/client.test.ts b/packages/network-debugger/src/legacy-bridge/client.test.ts new file mode 100644 index 0000000..478eec0 --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/client.test.ts @@ -0,0 +1,497 @@ +import { EventEmitter } from 'node:events' +import type { ChildProcess, ForkOptions } from 'node:child_process' +import { describe, expect, test, vi } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import type { LegacyCaptureEvent, LegacyChildMessage } from './contracts' +import { + LEGACY_BRIDGE_OPTIONS_ENV, + LegacyBridgeClient, + sanitizeLegacyEnvironment, + sanitizeLegacyExecArgv, + tokenizeNodeOptions +} from './client' + +class FakeChild extends EventEmitter { + pid = 42_000 + connected = true + killed = false + exitCode: number | null = null + signalCode: NodeJS.Signals | null = null + readonly sent: unknown[] = [] + readonly signals: Array = [] + readonly sendCallbacks: Array<(error: Error | null) => void> = [] + refCalls = 0 + unrefCalls = 0 + channelRefCalls = 0 + channelUnrefCalls = 0 + readonly channel = { + ref: () => { + this.channelRefCalls += 1 + }, + unref: () => { + this.channelUnrefCalls += 1 + } + } + + constructor( + private readonly autoDisposeAck = true, + private readonly manualSendCallbacks = false + ) { + super() + } + + send(message: unknown, callback?: (error: Error | null) => void): boolean { + if (!this.connected) throw new Error('disconnected') + this.sent.push(message) + if ( + this.autoDisposeAck && + message && + typeof message === 'object' && + 'type' in message && + (message as { type: unknown }).type === 'dispose' + ) { + queueMicrotask(() => { + this.emit('message', { type: 'disposed' } satisfies LegacyChildMessage) + this.exitCode = 0 + this.emit('exit', 0, null) + }) + } + if (callback) { + this.sendCallbacks.push(callback) + if (!this.manualSendCallbacks) queueMicrotask(() => callback(null)) + } + return true + } + + disconnect(): void { + this.connected = false + } + + ref(): void { + this.refCalls += 1 + } + + unref(): void { + this.unrefCalls += 1 + } + + kill(signal?: NodeJS.Signals | number): boolean { + this.signals.push(signal) + this.killed = true + this.connected = false + if (signal === 'SIGKILL') { + this.signalCode = 'SIGKILL' + queueMicrotask(() => this.emit('exit', null, 'SIGKILL')) + } + return true + } + + child(): ChildProcess { + return this as unknown as ChildProcess + } +} + +function target(port: number): DevtoolsTarget { + return { + id: 'legacy', + title: 'Legacy', + type: 'node', + url: '', + webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/legacy`, + discoveryUrl: `http://127.0.0.1:${port}/json/list` + } +} + +function ready(child: FakeChild, port: number): void { + child.emit('message', { type: 'ready', target: target(port) } satisfies LegacyChildMessage) +} + +function harness( + overrides: { + maxRestarts?: number + queueLimit?: number + autoDisposeAck?: boolean + shutdownGraceMs?: number + shutdownForceMs?: number + recoveryForceKillMs?: number + manualSendCallbacks?: boolean + } = {} +) { + const children: FakeChild[] = [] + const calls: Array<{ modulePath: string; args: string[]; options: ForkOptions }> = [] + const fork = vi.fn((modulePath: string, args: string[], options: ForkOptions) => { + const child = new FakeChild(overrides.autoDisposeAck, overrides.manualSendCallbacks) + children.push(child) + calls.push({ modulePath, args, options }) + return child.child() + }) + const client = new LegacyBridgeClient( + { host: '127.0.0.1', targetPort: 0 }, + { + fork, + childEntry: '/fixture/fork.js', + execArgv: [ + '--inspect-wait=127.0.0.1:0', + '--experimental-network-inspection', + '--watch', + '--import=/project/dist/register.mjs', + '--import', + 'safe-observer', + '--max-old-space-size=2048' + ], + env: { + NODE_OPTIONS: '--trace-warnings --import="/project/dist/register.mjs" --require safe-hook' + }, + maxRestarts: overrides.maxRestarts, + queueLimit: overrides.queueLimit, + shutdownGraceMs: overrides.shutdownGraceMs, + shutdownForceMs: overrides.shutdownForceMs, + recoveryForceKillMs: overrides.recoveryForceKillMs + } + ) + return { client, children, calls, fork } +} + +describe('LegacyBridgeClient', () => { + test('forks synchronously with advanced IPC and sanitized child flags/environment', async () => { + const { client, children, calls } = harness() + + expect(children).toHaveLength(1) + expect(calls[0]).toMatchObject({ + modulePath: '/fixture/fork.js', + args: [], + options: { + serialization: 'advanced', + execArgv: ['--import', 'safe-observer', '--max-old-space-size=2048'], + stdio: ['inherit', 'inherit', 'inherit', 'ipc'] + } + }) + expect(calls[0].options.env?.NODE_OPTIONS).toBe('--trace-warnings --require safe-hook') + expect(JSON.parse(calls[0].options.env?.[LEGACY_BRIDGE_OPTIONS_ENV] ?? '{}')).toMatchObject({ + host: '127.0.0.1', + targetPort: 0 + }) + await client.dispose() + }) + + test('queues an immutable first event until ready and preserves Buffer values', async () => { + const { client, children } = harness() + const rawData = Buffer.from('original') + const event: LegacyCaptureEvent = { + type: 'responseData', + data: { id: 'request-1', rawData, statusCode: 200, headers: {} } + } + + await client.send(event) + rawData.fill(0) + expect(children[0].sent).toEqual([]) + + ready(children[0], 43101) + await expect(client.ready).resolves.toMatchObject({ webSocketDebuggerUrl: expect.any(String) }) + expect(children[0].sent).toHaveLength(1) + const sent = children[0].sent[0] as { + type: 'capture' + event: Extract + } + expect(sent.type).toBe('capture') + expect(Buffer.isBuffer(sent.event.data.rawData)).toBe(true) + expect(sent.event.data.rawData.toString()).toBe('original') + await client.dispose() + }) + + test('releases ready child references and restores them for explicit disposal', async () => { + const { client, children } = harness() + const child = children[0] + + expect(child.unrefCalls).toBe(0) + expect(child.channelUnrefCalls).toBe(0) + ready(child, 43108) + await client.ready + expect(child.unrefCalls).toBe(1) + expect(child.channelUnrefCalls).toBe(1) + + const disposing = client.dispose() + expect(child.refCalls).toBe(1) + expect(child.channelRefCalls).toBe(1) + await disposing + }) + + test('bounds startup capture while preserving the earliest request event', async () => { + const { client, children } = harness({ queueLimit: 1 }) + const diagnostics: string[] = [] + client.onDiagnostic((diagnostic) => diagnostics.push(diagnostic.code)) + const first = { type: 'initRequest', data: { id: 'first' } } as LegacyCaptureEvent + const overflow = { type: 'initRequest', data: { id: 'overflow' } } as LegacyCaptureEvent + + await client.send(first) + await client.send(overflow) + ready(children[0], 43102) + + const sent = children[0].sent[0] as { event: { data: { id: string } } } + expect(sent.event.data.id).toBe('first') + expect(diagnostics).toContain('NND_LEGACY_CAPTURE_QUEUE_FULL') + await client.dispose() + }) + + test('recovers on the original target port and flushes events captured during restart', async () => { + const { client, children, calls } = harness() + const diagnostics: string[] = [] + client.onDiagnostic((diagnostic) => diagnostics.push(diagnostic.code)) + ready(children[0], 43103) + await client.ready + + children[0].emit('exit', 1, null) + expect(children).toHaveLength(2) + expect(JSON.parse(calls[1].options.env?.[LEGACY_BRIDGE_OPTIONS_ENV] ?? '{}').targetPort).toBe( + 43103 + ) + expect(JSON.parse(calls[1].options.env?.[LEGACY_BRIDGE_OPTIONS_ENV] ?? '{}').targetId).toBe( + JSON.parse(calls[0].options.env?.[LEGACY_BRIDGE_OPTIONS_ENV] ?? '{}').targetId + ) + await client.send({ type: 'initRequest', data: { id: 'during-restart' } } as LegacyCaptureEvent) + expect(children[1].sent).toEqual([]) + + ready(children[1], 43103) + expect((children[1].sent[0] as any).event.data.id).toBe('during-restart') + expect(diagnostics).toEqual( + expect.arrayContaining(['NND_LEGACY_CHILD_RESTARTING', 'NND_LEGACY_CHILD_RECOVERED']) + ) + await client.dispose() + }) + + test('waits for an errored child to exit before rebinding instead of exhausting retries', async () => { + const { client, children, calls } = harness({ recoveryForceKillMs: 50 }) + ready(children[0], 43106) + await client.ready + const first = children[0] + + first.emit('error', new Error('ipc failure while process is still alive')) + expect(first.signals).toEqual(['SIGTERM']) + expect(children).toHaveLength(1) + await client.send({ + type: 'initRequest', + data: { id: 'held-until-exit' } + } as LegacyCaptureEvent) + + first.emit('exit', 1, null) + expect(children).toHaveLength(2) + expect(JSON.parse(calls[1].options.env?.[LEGACY_BRIDGE_OPTIONS_ENV] ?? '{}').targetPort).toBe( + 43106 + ) + expect(children[1].sent).toEqual([]) + ready(children[1], 43106) + expect((children[1].sent[0] as any).event.data.id).toBe('held-until-exit') + await client.dispose() + }) + + test('requeues async send callback failures in order and starts only one recovery', async () => { + const { client, children } = harness({ + manualSendCallbacks: true, + recoveryForceKillMs: 50 + }) + const diagnostics: string[] = [] + client.onDiagnostic((diagnostic) => diagnostics.push(diagnostic.code)) + ready(children[0], 43107) + await client.ready + const first = children[0] + + await client.send({ type: 'initRequest', data: { id: 'send-a' } } as LegacyCaptureEvent) + await client.send({ type: 'initRequest', data: { id: 'send-b' } } as LegacyCaptureEvent) + expect(first.sendCallbacks).toHaveLength(2) + + first.sendCallbacks[0](new Error('first async send failed')) + first.sendCallbacks[1](new Error('second async send failed')) + expect(first.signals).toEqual(['SIGTERM']) + expect(first.listenerCount('exit')).toBe(1) + expect(children).toHaveLength(1) + + first.emit('exit', 1, null) + expect(children).toHaveLength(2) + expect(diagnostics.filter((code) => code === 'NND_LEGACY_CHILD_RESTARTING')).toHaveLength(1) + ready(children[1], 43107) + const replayedIds = children[1].sent + .filter((message: any) => message.type === 'capture') + .map((message: any) => message.event.data.id) + expect(replayedIds).toEqual(['send-a', 'send-b']) + await client.dispose() + }) + + test('rejects initial ready after exactly three bounded restart attempts', async () => { + const { client, children } = harness({ maxRestarts: 3 }) + const diagnostics: string[] = [] + const onFailure = vi.fn() + client.onDiagnostic((diagnostic) => diagnostics.push(diagnostic.code)) + client.onFailure(onFailure) + for (let index = 0; index < 4; index += 1) { + children[index].emit('exit', 1, null) + } + + expect(children).toHaveLength(4) + await expect(client.ready).rejects.toMatchObject({ + code: 'NND_LEGACY_CHILD_START_FAILED' + }) + expect(diagnostics.filter((code) => code === 'NND_LEGACY_CHILD_RESTARTING')).toHaveLength(3) + expect(diagnostics.at(-1)).toBe('NND_LEGACY_CHILD_START_FAILED') + expect(onFailure).toHaveBeenCalledOnce() + expect(onFailure).toHaveBeenCalledWith( + expect.objectContaining({ code: 'NND_LEGACY_CHILD_START_FAILED' }) + ) + const replayedFailure = vi.fn() + client.onFailure(replayedFailure) + expect(replayedFailure).toHaveBeenCalledWith( + expect.objectContaining({ code: 'NND_LEGACY_CHILD_START_FAILED' }) + ) + }) + + test('forwards and replays structured diagnostics reported by the child', async () => { + const { client, children } = harness() + children[0].emit('message', { + type: 'diagnostic', + diagnostic: { + code: 'NND_LEGACY_CHILD_NOTICE', + level: 'warn', + message: 'child notice', + details: { generation: 1 } + } + } satisfies LegacyChildMessage) + + const listener = vi.fn() + client.onDiagnostic(listener) + expect(listener).toHaveBeenCalledWith({ + code: 'NND_LEGACY_CHILD_NOTICE', + level: 'warn', + message: 'child notice', + details: { generation: 1 } + }) + await client.dispose() + }) + + test('never silently accepts a changed target during recovery', async () => { + const { client, children } = harness({ maxRestarts: 1 }) + const diagnostics: string[] = [] + const onFailure = vi.fn() + client.onDiagnostic((diagnostic) => diagnostics.push(diagnostic.code)) + client.onFailure(onFailure) + ready(children[0], 43104) + await client.ready + children[0].emit('exit', 1, null) + + ready(children[1], 43105) + // A mismatched child must fully exit before the fixed port can be retried. + children[1].emit('exit', 1, null) + expect(children).toHaveLength(2) + expect(diagnostics).toEqual( + expect.arrayContaining(['NND_LEGACY_TARGET_CHANGED', 'NND_LEGACY_CHILD_RECOVERY_FAILED']) + ) + expect(onFailure).toHaveBeenCalledWith( + expect.objectContaining({ code: 'NND_LEGACY_CHILD_RECOVERY_FAILED' }) + ) + await client.dispose() + }) + + test('dispose clears queued work/listeners and cannot trigger a restart', async () => { + const { client, children } = harness() + await client.send({ type: 'initRequest', data: { id: 'queued' } } as LegacyCaptureEvent) + const first = children[0] + + await client.dispose() + expect(first.sent).toContainEqual({ type: 'dispose' }) + expect(first.connected).toBe(false) + expect(first.signals).toEqual([]) + expect(first.listenerCount('message')).toBe(0) + first.emit('exit', 1, null) + await client.send({ type: 'initRequest', data: { id: 'ignored' } } as LegacyCaptureEvent) + expect(children).toHaveLength(1) + }) + + test('uses bounded SIGTERM/SIGKILL fallback and waits for child exit without an ack', async () => { + const { client, children } = harness({ + autoDisposeAck: false, + shutdownGraceMs: 5, + shutdownForceMs: 10 + }) + const first = children[0] + + let resolved = false + const disposing = client.dispose().then(() => { + resolved = true + }) + first.emit('disconnect') + await Promise.resolve() + expect(resolved).toBe(false) + await disposing + + expect(first.sent).toContainEqual({ type: 'dispose' }) + expect(first.signals).toEqual(['SIGTERM', 'SIGKILL']) + expect(first.listenerCount('exit')).toBe(0) + expect(first.listenerCount('message')).toBe(0) + }) +}) + +describe('Legacy child option sanitization', () => { + test('removes inspector/watch/project preload/tsx while keeping unrelated flags', () => { + expect( + sanitizeLegacyExecArgv([ + '--inspect', + '--inspect-port', + '9229', + '--experimental-network-inspection', + '--watch-path=src', + '--watch-kill-signal', + 'SIGTERM', + '--import=node-network-devtools/register', + '--loader', + 'tsx', + '--require=safe-hook', + '--trace-warnings' + ]) + ).toEqual(['--require=safe-hook', '--trace-warnings']) + }) + + test('tokenizes quoted NODE_OPTIONS and sanitizes a copy without mutating the parent', () => { + const env = { + NODE_OPTIONS: '--import="/some path/dist/register.mjs" --require "safe hook"' + } + expect(tokenizeNodeOptions(env.NODE_OPTIONS)).toEqual([ + '--import=/some path/dist/register.mjs', + '--require', + 'safe hook' + ]) + const child = sanitizeLegacyEnvironment(env) + expect(child.NODE_OPTIONS).toBe('--require "safe hook"') + expect(env.NODE_OPTIONS).toContain('register.mjs') + }) + + test.each([ + [['-e', 'console.log("short eval")', '--trace-warnings']], + [['--eval', 'console.log("long eval")', '--trace-warnings']], + [['-e=console.log("equals eval")', '--trace-warnings']], + [['--eval=console.log("equals eval")', '--trace-warnings']], + [['-econsole.log("attached eval")', '--trace-warnings']], + [['-p', 'process.version', '--trace-warnings']], + [['--print', 'process.version', '--trace-warnings']], + [['-p=process.version', '--trace-warnings']], + [['--print=process.version', '--trace-warnings']], + [['-pprocess.version', '--trace-warnings']], + [['--input-type', 'module', '--trace-warnings']], + [['--input-type=module', '--trace-warnings']] + ])('removes parent-only string input options from %j', (execArgv) => { + expect(sanitizeLegacyExecArgv(execArgv)).toEqual(['--trace-warnings']) + }) + + test('preserves Windows path separators while still handling quoted spaces', () => { + const nodeOptions = String.raw`--require="C:\Program Files\safe\hook.cjs" --import=C:\repo\node-network-devtools\dist\register.mjs --trace-warnings` + expect(tokenizeNodeOptions(nodeOptions)).toEqual([ + String.raw`--require=C:\Program Files\safe\hook.cjs`, + String.raw`--import=C:\repo\node-network-devtools\dist\register.mjs`, + '--trace-warnings' + ]) + + const child = sanitizeLegacyEnvironment({ NODE_OPTIONS: nodeOptions }) + expect(child.NODE_OPTIONS).not.toContain('register.mjs') + expect(tokenizeNodeOptions(child.NODE_OPTIONS ?? '')).toEqual([ + String.raw`--require=C:\Program Files\safe\hook.cjs`, + '--trace-warnings' + ]) + }) +}) diff --git a/packages/network-debugger/src/legacy-bridge/client.ts b/packages/network-debugger/src/legacy-bridge/client.ts new file mode 100644 index 0000000..ff511cd --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/client.ts @@ -0,0 +1,722 @@ +import { fork as nodeFork, type ChildProcess, type ForkOptions } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { resolve as resolvePath } from 'node:path' +import { deserialize, serialize } from 'node:v8' +import { __dirname } from '../common' +import type { DevtoolsTarget, Diagnostic } from '../adapters/types' +import type { + LegacyBridgeOptions, + LegacyCaptureEvent, + LegacyChildMessage, + LegacyParentMessage +} from './contracts' + +const DEFAULT_MAX_RESTARTS = 3 +const DEFAULT_QUEUE_LIMIT = 1_024 +const DIAGNOSTIC_HISTORY_LIMIT = 64 +export const LEGACY_BRIDGE_OPTIONS_ENV = 'NND_LEGACY_BRIDGE_OPTIONS' + +type ForkProcess = (modulePath: string, args: string[], options: ForkOptions) => ChildProcess + +export interface LegacyBridgeClientDependencies { + fork?: ForkProcess + childEntry?: string + execArgv?: readonly string[] + env?: NodeJS.ProcessEnv + maxRestarts?: number + queueLimit?: number + shutdownGraceMs?: number + shutdownForceMs?: number + recoveryForceKillMs?: number +} + +export type DiagnosticListener = (diagnostic: Diagnostic) => void +export type FailureListener = (error: LegacyBridgeError) => void + +interface QueuedParentMessage { + sequence: number + message: LegacyParentMessage +} + +export class LegacyBridgeError extends Error { + constructor( + readonly code: + | 'NND_LEGACY_CHILD_START_FAILED' + | 'NND_LEGACY_CHILD_RECOVERY_FAILED' + | 'NND_LEGACY_DISPOSED', + message: string, + readonly details: Readonly> = {} + ) { + super(message) + this.name = 'LegacyBridgeError' + } +} + +function optionName(argument: string): string { + const separator = argument.indexOf('=') + return separator === -1 ? argument : argument.slice(0, separator) +} + +function optionValue(argument: string): string | undefined { + const separator = argument.indexOf('=') + return separator === -1 ? undefined : argument.slice(separator + 1) +} + +function isProjectPreload(specifier: string): boolean { + const normalized = specifier.replace(/\\/g, '/').toLowerCase() + return ( + normalized === 'tsx' || + normalized.startsWith('tsx/') || + normalized.includes('/node_modules/tsx/') || + normalized === 'node-network-devtools/register' || + normalized === 'node-network-devtools/preload' || + normalized.includes('/node-network-devtools/dist/register.mjs') || + normalized.includes('/node-network-devtools/src/preload/register.') || + normalized.includes('/node-network-devtools/src/preload/index.') || + /(?:^|\/)dist\/register\.mjs(?:[?#].*)?$/.test(normalized) || + /(?:^|\/)src\/preload\/(?:register|index)\.[cm]?[jt]s(?:[?#].*)?$/.test(normalized) + ) +} + +/** + * Remove parent-only runtime flags before forking the Legacy backend. + * + * The preload process marker remains a second line of defence, but correctness + * does not depend on an inherited environment claim: the child simply never + * receives the recursive preload, Inspector pause, or watch flags. + */ +export function sanitizeLegacyExecArgv(execArgv: readonly string[]): string[] { + const sanitized: string[] = [] + + for (let index = 0; index < execArgv.length; index += 1) { + const argument = execArgv[index] + const name = optionName(argument) + const attachedValue = optionValue(argument) + + const isEvalOrPrint = name === '-e' || name === '--eval' || name === '-p' || name === '--print' + const isAttachedShortEvalOrPrint = + !argument.startsWith('--') && + (argument.startsWith('-e') || argument.startsWith('-p')) && + argument.length > 2 + if (isEvalOrPrint || isAttachedShortEvalOrPrint) { + if (isEvalOrPrint && attachedValue === undefined && execArgv[index + 1] !== undefined) + index += 1 + continue + } + + // `--input-type` is meaningful only for string/stdin input and makes a + // forked file entry fail once its inherited eval/print option is removed. + if (name === '--input-type') { + if (attachedValue === undefined && execArgv[index + 1] !== undefined) index += 1 + continue + } + + if (name.startsWith('--inspect') || name === '--experimental-network-inspection') { + if ( + attachedValue === undefined && + ['--inspect-port', '--inspect-publish-uid'].includes(name) && + execArgv[index + 1] && + !execArgv[index + 1].startsWith('-') + ) { + index += 1 + } + continue + } + + if (name === '--watch' || name === '--watch-preserve-output') continue + if (name === '--watch-path' || name === '--watch-kill-signal') { + if (attachedValue === undefined && execArgv[index + 1]) index += 1 + continue + } + + if ( + name === '--import' || + name === '--require' || + name === '-r' || + name === '--loader' || + name === '--experimental-loader' + ) { + const separateValue = attachedValue === undefined ? execArgv[index + 1] : undefined + const preload = attachedValue ?? separateValue + if (preload !== undefined && isProjectPreload(preload)) { + if (attachedValue === undefined) index += 1 + continue + } + } + + sanitized.push(argument) + } + + return sanitized +} + +/** A small NODE_OPTIONS tokenizer supporting the quoting accepted by Node. */ +export function tokenizeNodeOptions(value: string): string[] { + const tokens: string[] = [] + let token = '' + let quote: '"' | "'" | undefined + + for (let index = 0; index < value.length; index += 1) { + const character = value[index] + if (character === '\\') { + const next = value[index + 1] + const escapesOutsideQuote = + !quote && next !== undefined && (/\s/.test(next) || /['"]/.test(next)) + const escapesActiveQuote = quote !== undefined && next === quote + const escapesBackslash = next === '\\' + if (escapesOutsideQuote || escapesActiveQuote || escapesBackslash) { + token += next + index += 1 + } else { + // Backslashes are ordinary path separators unless they escape a quote + // or unquoted whitespace. In particular, preserve `C:\path` verbatim. + token += character + } + continue + } + if (quote) { + if (character === quote) quote = undefined + else token += character + continue + } + if (character === '"' || character === "'") { + quote = character + continue + } + if (/\s/.test(character)) { + if (token) { + tokens.push(token) + token = '' + } + continue + } + token += character + } + + if (token) tokens.push(token) + return tokens +} + +function quoteNodeOption(argument: string): string { + return /^[A-Za-z0-9_./:@%+,=-]+$/.test(argument) ? argument : JSON.stringify(argument) +} + +export function sanitizeLegacyEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const childEnv = { ...env } + const nodeOptions = childEnv.NODE_OPTIONS + if (nodeOptions) { + const sanitized = sanitizeLegacyExecArgv(tokenizeNodeOptions(nodeOptions)) + if (sanitized.length === 0) delete childEnv.NODE_OPTIONS + else childEnv.NODE_OPTIONS = sanitized.map(quoteNodeOption).join(' ') + } + return childEnv +} + +function snapshot(value: T): T { + // child_process IPC with `serialization: advanced` uses the same V8 value + // model. Snapshotting queued events now prevents mutable RequestDetail + // instances from collapsing init/register/end into their final state while + // preserving Buffer values as Buffers. + return deserialize(serialize(value)) as T +} + +function targetPort(target: DevtoolsTarget): number | undefined { + try { + const port = Number(new URL(target.webSocketDebuggerUrl).port) + return Number.isInteger(port) && port > 0 ? port : undefined + } catch { + return undefined + } +} + +function sameTargetEndpoint(expected: DevtoolsTarget, actual: DevtoolsTarget): boolean { + return ( + expected.webSocketDebuggerUrl === actual.webSocketDebuggerUrl && + expected.discoveryUrl === actual.discoveryUrl + ) +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +/** Parent-side owner of the Legacy child and its IPC transport. */ +export class LegacyBridgeClient { + readonly ready: Promise + + private readonly options: LegacyBridgeOptions + private readonly forkProcess: ForkProcess + private readonly childEntry: string + private readonly baseExecArgv: readonly string[] + private readonly baseEnv: NodeJS.ProcessEnv + private readonly maxRestarts: number + private readonly queueLimit: number + private readonly shutdownGraceMs: number + private readonly shutdownForceMs: number + private readonly recoveryForceKillMs: number + private readonly queue: QueuedParentMessage[] = [] + private readonly diagnosticListeners = new Set() + private readonly failureListeners = new Set() + private readonly diagnosticHistory: Diagnostic[] = [] + private child?: ChildProcess + private resolveReady!: (target: DevtoolsTarget) => void + private rejectReady!: (error: Error) => void + private firstTarget?: DevtoolsTarget + private currentTargetPort: number + private currentReady = false + private restartCount = 0 + private initialReadySettled = false + private terminal = false + private disposed = false + private disposePromise?: Promise + private recoveryKillTimer?: ReturnType + private recoveryGeneration?: number + private terminalFailure?: LegacyBridgeError + private generation = 0 + private messageSequence = 0 + + constructor(options: LegacyBridgeOptions, dependencies: LegacyBridgeClientDependencies = {}) { + this.options = { + ...options, + targetId: options.targetId ?? `node-network-devtools-${randomUUID()}` + } + this.currentTargetPort = options.targetPort + this.forkProcess = dependencies.fork ?? (nodeFork as ForkProcess) + this.childEntry = dependencies.childEntry ?? resolvePath(__dirname, './fork') + this.baseExecArgv = dependencies.execArgv ?? process.execArgv + this.baseEnv = dependencies.env ?? process.env + this.maxRestarts = dependencies.maxRestarts ?? DEFAULT_MAX_RESTARTS + this.queueLimit = Math.max(1, dependencies.queueLimit ?? DEFAULT_QUEUE_LIMIT) + this.shutdownGraceMs = Math.max(0, dependencies.shutdownGraceMs ?? 1_000) + this.shutdownForceMs = Math.max(this.shutdownGraceMs, dependencies.shutdownForceMs ?? 1_500) + this.recoveryForceKillMs = Math.max(0, dependencies.recoveryForceKillMs ?? 500) + this.ready = new Promise((resolve, reject) => { + this.resolveReady = resolve + this.rejectReady = reject + }) + void this.ready.catch(() => undefined) + + // Fork synchronously so `register(); request()` cannot race process setup. + this.spawnChild() + } + + onDiagnostic(listener: DiagnosticListener): () => void { + this.diagnosticListeners.add(listener) + for (const diagnostic of this.diagnosticHistory) listener(diagnostic) + return () => this.diagnosticListeners.delete(listener) + } + + onFailure(listener: FailureListener): () => void { + this.failureListeners.add(listener) + if (this.terminalFailure) { + try { + listener(this.terminalFailure) + } catch { + // Replayed failure listeners have the same isolation as live ones. + } + } + return () => this.failureListeners.delete(listener) + } + + async send(event: LegacyCaptureEvent): Promise { + if (this.disposed || this.terminal) return + + let message: LegacyParentMessage + try { + message = snapshot({ type: 'capture', event } satisfies LegacyParentMessage) + } catch (error) { + this.emitDiagnostic({ + code: 'NND_LEGACY_CAPTURE_SERIALIZATION_FAILED', + level: 'error', + message: `A Legacy capture event could not be serialized: ${errorMessage(error)}`, + hint: 'Remove stream, socket, function, or other live transport objects from capture data.', + details: { eventType: event.type } + }) + return + } + + const queuedMessage = { sequence: ++this.messageSequence, message } + if (!this.currentReady || !this.child?.connected) { + this.enqueue(queuedMessage) + return + } + this.sendToChild(queuedMessage) + } + + async dispose(): Promise { + if (this.disposePromise) return this.disposePromise + this.disposed = true + this.currentReady = false + this.queue.length = 0 + if (this.recoveryKillTimer) { + clearTimeout(this.recoveryKillTimer) + this.recoveryKillTimer = undefined + } + this.recoveryGeneration = undefined + + if (!this.initialReadySettled) { + this.initialReadySettled = true + this.rejectReady( + new LegacyBridgeError( + 'NND_LEGACY_DISPOSED', + 'The Legacy bridge was disposed before its target became ready.' + ) + ) + } + + const child = this.child + this.child = undefined + this.disposePromise = child ? this.shutdownChild(child) : Promise.resolve() + await this.disposePromise + this.diagnosticListeners.clear() + this.failureListeners.clear() + this.diagnosticHistory.length = 0 + } + + private spawnChild(): void { + if (this.disposed || this.terminal) return + const generation = ++this.generation + this.currentReady = false + const childEnv = sanitizeLegacyEnvironment(this.baseEnv) + childEnv[LEGACY_BRIDGE_OPTIONS_ENV] = JSON.stringify({ + ...this.options, + targetPort: this.currentTargetPort + } satisfies LegacyBridgeOptions) + + let child: ChildProcess + try { + child = this.forkProcess(this.childEntry, [], { + env: childEnv, + execArgv: sanitizeLegacyExecArgv(this.baseExecArgv), + serialization: 'advanced', + stdio: ['inherit', 'inherit', 'inherit', 'ipc'] + }) + } catch (error) { + this.handleChildFailure(undefined, generation, { error: errorMessage(error) }) + return + } + + this.child = child + let failed = false + const failOnce = (details: Readonly>) => { + if (failed) return + failed = true + this.handleChildFailure(child, generation, details) + } + + child.on('message', (message: unknown) => { + if (generation !== this.generation || this.disposed || this.terminal) return + this.handleChildMessage(child, generation, message) + }) + child.once('error', (error) => { + if (failed) return + failed = true + const details = { error: errorMessage(error) } + if (child.pid === undefined) this.handleChildFailure(child, generation, details) + else this.terminateChildThenHandleFailure(child, generation, details) + }) + child.once('exit', (code, signal) => failOnce({ code, signal })) + } + + private handleChildMessage(child: ChildProcess, generation: number, message: unknown): void { + if (!message || typeof message !== 'object' || !('type' in message)) return + const childMessage = message as LegacyChildMessage + + if (childMessage.type === 'diagnostic') { + this.emitDiagnostic(childMessage.diagnostic) + return + } + if (childMessage.type === 'disposed') return + if (childMessage.type !== 'ready') return + + const target = childMessage.target + if (this.firstTarget && !sameTargetEndpoint(this.firstTarget, target)) { + this.emitDiagnostic({ + code: 'NND_LEGACY_TARGET_CHANGED', + level: 'error', + message: 'The recovered Legacy child reported a different DevTools target.', + hint: 'Keep the original target port available while the Legacy session is active.', + details: { + expected: this.firstTarget.webSocketDebuggerUrl, + actual: target.webSocketDebuggerUrl, + restartCount: this.restartCount + } + }) + this.terminateChildThenHandleFailure(child, generation, { reason: 'target-changed' }) + return + } + + const boundPort = targetPort(target) + if (!boundPort) { + this.terminateChildThenHandleFailure(child, generation, { reason: 'invalid-target-port' }) + return + } + + if (!this.firstTarget) { + this.firstTarget = target + this.currentTargetPort = boundPort + this.initialReadySettled = true + this.resolveReady(target) + } else if (this.restartCount > 0) { + this.emitDiagnostic({ + code: 'NND_LEGACY_CHILD_RECOVERED', + level: 'info', + message: 'The Legacy backend recovered on its original DevTools target.', + details: { restartCount: this.restartCount, target: target.webSocketDebuggerUrl } + }) + } + + this.currentReady = true + this.flushQueue() + // The debugger backend is auxiliary: once its target is usable, neither + // the child handle nor its IPC channel may keep an otherwise finite Node + // application alive. The child observes the eventual IPC disconnect and + // closes its target before exiting. + child.unref() + child.channel?.unref() + } + + private handleChildFailure( + child: ChildProcess | undefined, + generation: number, + details: Readonly> + ): void { + if (generation !== this.generation || this.disposed || this.terminal) return + this.currentReady = false + if (child) { + child.removeAllListeners() + if (child.connected) child.disconnect() + if (!child.killed) child.kill() + } + if (this.child === child) this.child = undefined + + if (this.restartCount < this.maxRestarts) { + this.restartCount += 1 + this.emitDiagnostic({ + code: 'NND_LEGACY_CHILD_RESTARTING', + level: 'warn', + message: 'The Legacy backend exited unexpectedly; restarting it.', + hint: this.firstTarget + ? 'The recovered child will bind the original DevTools target port.' + : 'Capture events remain queued while the initial target starts.', + details: { + restartCount: this.restartCount, + maxRestarts: this.maxRestarts, + phase: this.firstTarget ? 'runtime' : 'startup', + ...details + } + }) + this.spawnChild() + return + } + + this.terminal = true + this.queue.length = 0 + const code = this.firstTarget + ? 'NND_LEGACY_CHILD_RECOVERY_FAILED' + : 'NND_LEGACY_CHILD_START_FAILED' + const message = this.firstTarget + ? `The Legacy backend could not be recovered after ${this.maxRestarts} restart attempts.` + : `The Legacy backend could not start after ${this.maxRestarts} restart attempts.` + const error = new LegacyBridgeError(code, message, { + restartCount: this.restartCount, + maxRestarts: this.maxRestarts, + ...details + }) + this.emitDiagnostic({ + code, + level: 'error', + message, + hint: 'Inspect the child diagnostics and verify that the configured target port is available.', + details: error.details + }) + this.emitFailure(error) + if (!this.initialReadySettled) { + this.initialReadySettled = true + this.rejectReady(error) + } + } + + private enqueue(queuedMessage: QueuedParentMessage): void { + if (this.queue.some(({ sequence }) => sequence === queuedMessage.sequence)) return + this.queue.push(queuedMessage) + this.queue.sort((left, right) => left.sequence - right.sequence) + const dropped = this.queue.length > this.queueLimit ? this.queue.pop() : undefined + if (dropped) { + const { message } = dropped + this.emitDiagnostic({ + code: 'NND_LEGACY_CAPTURE_QUEUE_FULL', + level: 'warn', + message: `The Legacy startup queue reached its ${this.queueLimit}-event limit.`, + hint: 'Wait for the Legacy target to become ready before producing additional traffic.', + details: { + queueLimit: this.queueLimit, + droppedEventType: message.type === 'capture' ? message.event.type : message.type + } + }) + } + } + + private flushQueue(): void { + while (this.currentReady && this.child?.connected && this.queue.length > 0) { + const queuedMessage = this.queue.shift()! + this.sendToChild(queuedMessage) + } + } + + private sendToChild(queuedMessage: QueuedParentMessage): void { + const child = this.child + if (!child || !child.connected) { + this.enqueue(queuedMessage) + return + } + const generation = this.generation + let failed = false + const fail = (error: unknown, source: 'send-callback' | 'send-throw') => { + if (failed || this.disposed || this.terminal) return + failed = true + this.enqueue(queuedMessage) + if (generation === this.generation && child === this.child) { + this.terminateChildThenHandleFailure(child, generation, { + error: errorMessage(error), + source + }) + } else if (this.currentReady) { + this.flushQueue() + } + } + try { + child.send(queuedMessage.message, (error) => { + if (error) fail(error, 'send-callback') + }) + } catch (error) { + fail(error, 'send-throw') + } + } + + /** Wait for the old process to exit before rebinding its target port. */ + private terminateChildThenHandleFailure( + child: ChildProcess, + generation: number, + details: Readonly> + ): void { + if (generation !== this.generation || this.disposed || this.terminal) return + if (this.recoveryGeneration === generation) return + this.recoveryGeneration = generation + this.currentReady = false + child.removeAllListeners() + let completed = false + const complete = () => { + if (completed) return + completed = true + if (this.recoveryKillTimer) { + clearTimeout(this.recoveryKillTimer) + this.recoveryKillTimer = undefined + } + if (this.recoveryGeneration === generation) this.recoveryGeneration = undefined + this.handleChildFailure(child, generation, details) + } + child.once('exit', complete) + + if (child.exitCode !== null && child.exitCode !== undefined) { + complete() + return + } + if (child.connected) child.disconnect() + try { + child.kill('SIGTERM') + } catch { + // The exit listener remains authoritative if signalling races exit. + } + if (completed) return + this.recoveryKillTimer = setTimeout(() => { + if (!completed) child.kill('SIGKILL') + }, this.recoveryForceKillMs) + this.recoveryKillTimer.unref?.() + } + + private shutdownChild(child: ChildProcess): Promise { + // A caller explicitly awaiting dispose expects the acknowledgement/exit + // handshake to complete. Restore both references in case readiness + // detached them from the event loop. + child.ref() + if (child.connected) child.channel?.ref() + + if (!child.connected && (child.exitCode != null || child.signalCode != null)) { + child.removeAllListeners() + return Promise.resolve() + } + + return new Promise((resolve) => { + let settled = false + let graceTimer: ReturnType | undefined + let forceTimer: ReturnType | undefined + + const finish = () => { + if (settled) return + settled = true + if (graceTimer) clearTimeout(graceTimer) + if (forceTimer) clearTimeout(forceTimer) + child.removeAllListeners() + if (child.connected) child.disconnect() + resolve() + } + const onMessage = (message: unknown) => { + if ( + message && + typeof message === 'object' && + 'type' in message && + (message as LegacyChildMessage).type === 'disposed' + ) { + // The host acknowledges only after RequestCenter.close(), so its + // target port is already closed. Still wait for the actual exit; + // existing grace/force timers prevent an acknowledged orphan. + if (child.connected) child.disconnect() + } + } + + child.on('message', onMessage) + child.once('exit', finish) + + try { + if (child.connected) child.send({ type: 'dispose' } satisfies LegacyParentMessage) + } catch { + child.kill('SIGTERM') + } + + if (settled) return + graceTimer = setTimeout(() => { + if (!settled) child.kill('SIGTERM') + }, this.shutdownGraceMs) + forceTimer = setTimeout(() => { + if (!settled) child.kill('SIGKILL') + }, this.shutdownForceMs) + graceTimer.unref?.() + forceTimer.unref?.() + }) + } + + private emitDiagnostic(diagnostic: Diagnostic): void { + this.diagnosticHistory.push(diagnostic) + if (this.diagnosticHistory.length > DIAGNOSTIC_HISTORY_LIMIT) this.diagnosticHistory.shift() + for (const listener of this.diagnosticListeners) { + try { + listener(diagnostic) + } catch { + // A consumer diagnostic callback must never destabilize capture. + } + } + } + + private emitFailure(error: LegacyBridgeError): void { + if (this.terminalFailure) return + this.terminalFailure = error + for (const listener of this.failureListeners) { + try { + listener(error) + } catch { + // A consumer failure callback must never destabilize teardown. + } + } + } +} diff --git a/packages/network-debugger/src/legacy-bridge/contracts.ts b/packages/network-debugger/src/legacy-bridge/contracts.ts new file mode 100644 index 0000000..75d9b3c --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/contracts.ts @@ -0,0 +1,105 @@ +import type { IncomingHttpHeaders } from 'node:http' +import type { Socket } from 'node:net' +import type { RequestDetail } from '../common' +import type { DevtoolsTarget, Diagnostic } from '../adapters/types' + +export type CdpId = number | string + +export type LegacyRequestEventType = + | 'initRequest' + | 'registerRequest' + | 'updateRequest' + | 'endRequest' + +export interface LegacyResponseData { + id: string + rawData: Buffer + statusCode: number + statusMessage?: string + headers: IncomingHttpHeaders | Record + contentEncoding?: string +} + +export interface LegacyRequestFailure { + request: RequestDetail + errorText: string + canceled?: boolean + blockedReason?: string +} + +/** A deliberately small, structured copy of an HTTP upgrade response. */ +export interface LegacyWebSocketHandshake { + httpVersion: string + statusCode: number + statusMessage: string + rawHeaders: readonly string[] + headers: IncomingHttpHeaders +} + +export interface LegacyWebSocketFrame { + requestId: string + response: { + payloadData: string + opcode: number + mask: boolean + } +} + +export type LegacyCaptureEvent = + | { type: LegacyRequestEventType; data: RequestDetail } + | { type: 'responseData'; data: LegacyResponseData } + | { type: 'responseReceived'; data: RequestDetail } + | { type: 'requestFailed'; data: LegacyRequestFailure } + | { type: 'eventSourceResponseReceived'; data: RequestDetail } + | { + type: 'eventSourceMessage' + data: { requestId: string; eventName: string; eventId: string; data: string } + } + | { + type: 'Network.webSocketCreated' + data: { + requestId: string + url: string + initiator?: RequestDetail['initiator'] + response: LegacyWebSocketHandshake + } + } + | { type: 'Network.webSocketFrameSent'; data: LegacyWebSocketFrame } + | { type: 'Network.webSocketFrameReceived'; data: LegacyWebSocketFrame } + | { type: 'Network.webSocketClosed'; data: { requestId: string } } + +export interface LegacyBridgeOptions { + host: string + targetPort: number + /** Stable across child recovery so the complete WebSocket URL cannot change. */ + targetId?: string + title?: string +} + +export type LegacyParentMessage = + | { type: 'capture'; event: LegacyCaptureEvent } + | { type: 'dispose' } + +export type LegacyChildMessage = + | { type: 'ready'; target: DevtoolsTarget } + | { type: 'diagnostic'; diagnostic: Diagnostic } + | { type: 'disposed' } + +export interface LegacyCaptureSink { + send(event: LegacyCaptureEvent): Promise + sendRequest(type: LegacyRequestEventType, request: RequestDetail): LegacyCaptureSink + responseRequest( + request: string | RequestDetail, + response: NodeJS.ReadableStream & { + statusCode?: number + statusMessage?: string + headers: IncomingHttpHeaders + } + ): void +} + +/** + * `Socket` is intentionally not part of any bridge event. This alias exists to + * make accidental transport-object leakage visible in capture code reviews. + */ +export type LegacyLocalSocket = Socket diff --git a/packages/network-debugger/src/legacy-bridge/host.test.ts b/packages/network-debugger/src/legacy-bridge/host.test.ts new file mode 100644 index 0000000..fa29369 --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/host.test.ts @@ -0,0 +1,138 @@ +import { EventEmitter } from 'node:events' +import { describe, expect, test, vi } from 'vitest' +import type { RequestCenter } from '../fork/request-center' +import type { DevtoolsTarget } from '../adapters/types' +import type { LegacyBridgeHostProcess } from './host' +import { runLegacyBridgeHost } from './host' +import { LEGACY_BRIDGE_OPTIONS_ENV } from './client' + +function deferred() { + let resolve!: (value: T) => void + let reject!: (error: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} + +class FakeHostProcess extends EventEmitter implements LegacyBridgeHostProcess { + connected = true + readonly sent: unknown[] = [] + readonly exit = vi.fn((_code?: number) => undefined) + readonly env = { + [LEGACY_BRIDGE_OPTIONS_ENV]: JSON.stringify({ + host: '127.0.0.1', + targetPort: 0, + targetId: 'stable-legacy-target', + title: 'Stable Legacy' + }) + } + + send(message: unknown): boolean { + this.sent.push(message) + return true + } + + disconnect(): void { + this.connected = false + } +} + +const target: DevtoolsTarget = { + id: 'legacy', + title: 'Legacy', + type: 'node', + url: '', + webSocketDebuggerUrl: 'ws://127.0.0.1:43110/devtools/page/legacy', + discoveryUrl: 'http://127.0.0.1:43110/json/list' +} + +describe('runLegacyBridgeHost', () => { + test('starts RequestCenter, routes capture in order, and closes on parent dispose', async () => { + const processLike = new FakeHostProcess() + const readiness = deferred() + const center = { + ready: readiness.promise, + handleCaptureEvent: vi.fn(), + close: vi.fn(async () => undefined) + } + const createCenter = vi.fn(() => center as unknown as RequestCenter) + const loadPlugins = vi.fn() + const running = runLegacyBridgeHost({ process: processLike, createCenter, loadPlugins }) + + processLike.emit('message', { + type: 'capture', + event: { type: 'initRequest', data: { id: 'before-ready' } } + }) + await Promise.resolve() + expect(center.handleCaptureEvent).toHaveBeenCalledWith( + expect.objectContaining({ type: 'initRequest' }) + ) + + readiness.resolve(target) + await running + expect(createCenter).toHaveBeenCalledWith({ + serverPort: 0, + targetId: 'stable-legacy-target', + title: 'Stable Legacy', + autoOpenDevtool: false + }) + expect(loadPlugins).toHaveBeenCalledWith(center) + expect(processLike.sent).toContainEqual({ type: 'ready', target }) + + processLike.emit('message', { type: 'dispose' }) + await vi.waitFor(() => expect(center.close).toHaveBeenCalledOnce()) + expect(processLike.sent).toContainEqual({ type: 'disposed' }) + expect(processLike.connected).toBe(false) + expect(processLike.listenerCount('message')).toBe(0) + expect(processLike.listenerCount('disconnect')).toBe(0) + expect(processLike.exit).toHaveBeenCalledWith(0) + }) + + test('closes and exits when the parent IPC channel disappears', async () => { + const processLike = new FakeHostProcess() + const center = { + ready: Promise.resolve(target), + handleCaptureEvent: vi.fn(), + close: vi.fn(async () => undefined) + } + + await runLegacyBridgeHost({ + process: processLike, + createCenter: () => center as unknown as RequestCenter, + loadPlugins: vi.fn() + }) + processLike.connected = false + processLike.emit('disconnect') + + await vi.waitFor(() => expect(center.close).toHaveBeenCalledOnce()) + expect(processLike.sent).not.toContainEqual({ type: 'disposed' }) + expect(processLike.exit).toHaveBeenCalledWith(0) + expect(processLike.listenerCount('message')).toBe(0) + expect(processLike.listenerCount('disconnect')).toBe(0) + }) + + test('sends a structured startup diagnostic and closes when target bind fails', async () => { + const processLike = new FakeHostProcess() + const center = { + ready: Promise.reject(new Error('address in use')), + handleCaptureEvent: vi.fn(), + close: vi.fn(async () => undefined) + } + + await expect( + runLegacyBridgeHost({ + process: processLike, + createCenter: () => center as unknown as RequestCenter, + loadPlugins: vi.fn() + }) + ).rejects.toThrow('address in use') + expect(processLike.sent).toContainEqual({ + type: 'diagnostic', + diagnostic: expect.objectContaining({ code: 'NND_LEGACY_TARGET_START_FAILED' }) + }) + expect(center.close).toHaveBeenCalledOnce() + expect(processLike.listenerCount('disconnect')).toBe(0) + }) +}) diff --git a/packages/network-debugger/src/legacy-bridge/host.ts b/packages/network-debugger/src/legacy-bridge/host.ts new file mode 100644 index 0000000..5f6c96c --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/host.ts @@ -0,0 +1,142 @@ +import type { Diagnostic } from '../adapters/types' +import { RequestCenter } from '../fork/request-center' +import { loadPlugin } from '../fork/module' +import type { LegacyBridgeOptions, LegacyChildMessage, LegacyParentMessage } from './contracts' +import { LEGACY_BRIDGE_OPTIONS_ENV } from './client' + +export interface LegacyBridgeHostProcess { + env: NodeJS.ProcessEnv + send?: (message: LegacyChildMessage, callback?: (error: Error | null) => void) => boolean + on(event: 'message', listener: (message: unknown) => void): unknown + on(event: 'disconnect', listener: () => void): unknown + off(event: 'message', listener: (message: unknown) => void): unknown + off(event: 'disconnect', listener: () => void): unknown + disconnect?: () => void + connected?: boolean + exit?: (code?: number) => never | void +} + +export interface LegacyBridgeHostDependencies { + process?: LegacyBridgeHostProcess + createCenter?: (options: { + serverPort?: number + targetId?: string + title?: string + autoOpenDevtool?: boolean + }) => RequestCenter + loadPlugins?: (center: RequestCenter) => void +} + +function parseOptions(env: NodeJS.ProcessEnv): LegacyBridgeOptions { + const serialized = env[LEGACY_BRIDGE_OPTIONS_ENV] + if (!serialized) throw new Error(`${LEGACY_BRIDGE_OPTIONS_ENV} is required.`) + const value = JSON.parse(serialized) as Partial + if (typeof value.host !== 'string' || typeof value.targetPort !== 'number') { + throw new Error(`${LEGACY_BRIDGE_OPTIONS_ENV} is invalid.`) + } + return { + host: value.host, + targetPort: value.targetPort, + ...(typeof value.targetId === 'string' ? { targetId: value.targetId } : {}), + ...(typeof value.title === 'string' ? { title: value.title } : {}) + } +} + +function send(processLike: LegacyBridgeHostProcess, message: LegacyChildMessage): void { + if (!processLike.send || processLike.connected === false) return + try { + processLike.send(message) + } catch { + // Parent teardown can race a final diagnostic/disposed acknowledgement. + } +} + +/** Start and own one RequestCenter inside the forked Legacy child. */ +export async function runLegacyBridgeHost( + dependencies: LegacyBridgeHostDependencies = {} +): Promise<() => Promise> { + const processLike = dependencies.process ?? (process as unknown as LegacyBridgeHostProcess) + const options = parseOptions(processLike.env) + const createCenter = + dependencies.createCenter ?? ((centerOptions) => new RequestCenter(centerOptions)) + const center = createCenter({ + serverPort: options.targetPort || 0, + ...(options.targetId ? { targetId: options.targetId } : {}), + ...(options.title ? { title: options.title } : {}), + autoOpenDevtool: false + }) + ;(dependencies.loadPlugins ?? loadPlugin)(center) + + let captureChain = Promise.resolve() + let closing: Promise | undefined + + const close = (notifyParent = true) => { + if (closing) return closing + closing = captureChain + .catch(() => undefined) + .then(() => center.close()) + .then(() => { + if (notifyParent && processLike.connected !== false) { + send(processLike, { type: 'disposed' }) + } + processLike.off('message', onMessage) + processLike.off('disconnect', onDisconnect) + if (processLike.connected !== false) processLike.disconnect?.() + // This module runs in a dedicated bridge process. Explicitly exiting + // after all owned resources have closed prevents an IPC-disconnected + // child from becoming an orphan if its parent is terminated mid-dispose. + processLike.exit?.(0) + }) + return closing + } + + const onDisconnect = () => { + void close(false) + } + + const onMessage = (message: unknown) => { + if (!message || typeof message !== 'object' || !('type' in message)) return + const parentMessage = message as LegacyParentMessage + if (parentMessage.type === 'dispose') { + void close(true) + return + } + if (parentMessage.type === 'capture') { + captureChain = captureChain.then(() => { + center.handleCaptureEvent(parentMessage.event) + }) + void captureChain.catch((error) => { + const diagnostic: Diagnostic = { + code: 'NND_LEGACY_CAPTURE_FAILED', + level: 'error', + message: error instanceof Error ? error.message : String(error), + details: { eventType: parentMessage.event.type } + } + send(processLike, { type: 'diagnostic', diagnostic }) + }) + } + } + + processLike.on('message', onMessage) + processLike.on('disconnect', onDisconnect) + try { + const target = await center.ready + send(processLike, { type: 'ready', target }) + } catch (error) { + send(processLike, { + type: 'diagnostic', + diagnostic: { + code: 'NND_LEGACY_TARGET_START_FAILED', + level: 'error', + message: error instanceof Error ? error.message : String(error), + hint: 'Verify that the requested Legacy target port is available.' + } + }) + processLike.off('message', onMessage) + processLike.off('disconnect', onDisconnect) + await center.close().catch(() => undefined) + throw error + } + + return () => close(true) +} diff --git a/packages/network-debugger/src/legacy-bridge/index.ts b/packages/network-debugger/src/legacy-bridge/index.ts new file mode 100644 index 0000000..df6ef7e --- /dev/null +++ b/packages/network-debugger/src/legacy-bridge/index.ts @@ -0,0 +1,3 @@ +export * from './contracts' +export * from './client' +export * from './host' diff --git a/packages/network-debugger/src/mock/index.test.ts b/packages/network-debugger/src/mock/index.test.ts new file mode 100644 index 0000000..83adb93 --- /dev/null +++ b/packages/network-debugger/src/mock/index.test.ts @@ -0,0 +1,109 @@ +import type { ClientRequest, IncomingMessage } from 'node:http' +import { describe, expect, test, vi } from 'vitest' +import type { RequestFn } from '../core/request' +import { + findFetchMock, + findLegacyMock, + mockableRequestHandler, + mockedFetchResponse, + type LegacyMockRule +} from '.' + +const rules: readonly LegacyMockRule[] = [ + { + id: 'service-success', + match: { + url: 'http://127.0.0.1:*/service?*', + method: 'POST', + headers: { 'x-mode': 'mock' } + }, + response: { + status: 201, + statusText: 'Created by mock', + headers: { 'content-type': 'application/json', 'x-mock': 'yes' }, + body: '{"mocked":true}' + } + } +] + +describe('Legacy mocks', () => { + test('matches URL globs, methods, and case-insensitive request headers', () => { + expect( + findLegacyMock(rules, { + url: 'http://127.0.0.1:43100/service?value=1', + method: 'post', + headers: { 'X-Mode': 'mock' } + })?.id + ).toBe('service-success') + expect( + findLegacyMock(rules, { + url: 'http://127.0.0.1:43100/service?value=1', + method: 'GET', + headers: { 'x-mode': 'mock' } + }) + ).toBeUndefined() + }) + + test('returns an IncomingMessage-compatible HTTP response without calling the network', async () => { + const actual = vi.fn() as unknown as RequestFn + const handler = mockableRequestHandler(actual, false, rules) + const response = new Promise<{ + status?: number + headers: IncomingMessage['headers'] + body: string + }>((resolve) => { + const request = handler( + 'http://127.0.0.1:43100/service?value=1', + { method: 'POST', headers: { 'x-mode': 'mock' } }, + (incoming) => { + const chunks: Buffer[] = [] + incoming.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + incoming.on('end', () => + resolve({ + status: incoming.statusCode, + headers: incoming.headers, + body: Buffer.concat(chunks).toString() + }) + ) + } + ) + request.write('ignored request payload') + request.end() + }) + + await expect(response).resolves.toEqual({ + status: 201, + headers: { + 'content-type': 'application/json', + 'x-mock': 'yes', + 'content-length': '15' + }, + body: '{"mocked":true}' + }) + expect(actual).not.toHaveBeenCalled() + }) + + test('delegates unmatched HTTP requests with their original overload', () => { + const expected = {} as ClientRequest + const actual = vi.fn(() => expected) as unknown as RequestFn + const handler = mockableRequestHandler(actual, false, rules) + const callback = vi.fn() + + expect(handler('http://127.0.0.1:43100/real', callback)).toBe(expected) + expect(actual).toHaveBeenCalledWith('http://127.0.0.1:43100/real', callback) + }) + + test('mocks Fetch and supports a serialized binary response body', async () => { + const binaryRule: LegacyMockRule = { + match: { url: 'https://example.test/binary' }, + response: { + status: 200, + headers: { 'content-type': 'application/octet-stream' }, + bodyBase64: Buffer.from([0, 1, 2, 255]).toString('base64') + } + } + expect(findFetchMock([binaryRule], 'https://example.test/binary')).toBe(binaryRule) + const response = await mockedFetchResponse(binaryRule) + expect(Buffer.from(await response.arrayBuffer())).toEqual(Buffer.from([0, 1, 2, 255])) + }) +}) diff --git a/packages/network-debugger/src/mock/index.ts b/packages/network-debugger/src/mock/index.ts new file mode 100644 index 0000000..9fe3a37 --- /dev/null +++ b/packages/network-debugger/src/mock/index.ts @@ -0,0 +1,329 @@ +import { Readable, Writable } from 'node:stream' +import type { ClientRequest, IncomingHttpHeaders, IncomingMessage, RequestOptions } from 'node:http' +import type { RequestFn } from '../core/request' + +export interface LegacyMockMatcher { + /** Exact URL, or a glob containing `*` wildcards. */ + url: string + method?: string + headers?: Readonly> +} + +export interface LegacyMockResponse { + status?: number + statusText?: string + headers?: Readonly> + body?: string + /** Binary response body encoded as base64. Mutually exclusive with `body`. */ + bodyBase64?: string + delayMs?: number +} + +export interface LegacyMockRule { + id?: string + match: LegacyMockMatcher + response: LegacyMockResponse +} + +export interface LegacyMockRequest { + url: string + method: string + headers: Readonly> +} + +function globExpression(pattern: string): RegExp { + const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, '\\$&').replace(/\*/g, '.*') + return new RegExp(`^${escaped}$`) +} + +function normalizedHeaders(headers: LegacyMockRequest['headers']): Record { + return Object.fromEntries( + Object.entries(headers).flatMap(([name, value]) => { + if (value === undefined) return [] + return [[name.toLowerCase(), Array.isArray(value) ? value.join(', ') : String(value)]] + }) + ) +} + +export function findLegacyMock( + rules: readonly LegacyMockRule[], + request: LegacyMockRequest +): LegacyMockRule | undefined { + const requestHeaders = normalizedHeaders(request.headers) + return rules.find((rule) => { + if (!rule.match.url || !globExpression(rule.match.url).test(request.url)) return false + if (rule.match.method && rule.match.method.toUpperCase() !== request.method.toUpperCase()) { + return false + } + return Object.entries(rule.match.headers ?? {}).every( + ([name, value]) => requestHeaders[name.toLowerCase()] === value + ) + }) +} + +function responseHeaders(values: LegacyMockResponse['headers']): IncomingHttpHeaders { + const headers: IncomingHttpHeaders = {} + for (const [name, value] of Object.entries(values ?? {})) { + headers[name.toLowerCase()] = typeof value === 'string' ? value : [...value] + } + return headers +} + +function rawHeaders(values: IncomingHttpHeaders): string[] { + return Object.entries(values).flatMap(([name, value]) => { + if (value === undefined) return [] + return Array.isArray(value) ? value.flatMap((nested) => [name, nested]) : [name, String(value)] + }) +} + +class MockIncomingMessage extends Readable { + readonly statusCode: number + readonly statusMessage: string + readonly headers: IncomingHttpHeaders + readonly rawHeaders: string[] + readonly httpVersion = '1.1' + complete = false + private sent = false + + constructor(response: LegacyMockResponse) { + super() + this.statusCode = response.status ?? 200 + this.statusMessage = response.statusText ?? 'Mocked' + this.headers = responseHeaders(response.headers) + const body = response.bodyBase64 + ? Buffer.from(response.bodyBase64, 'base64') + : Buffer.from(response.body ?? '') + if (this.headers['content-length'] === undefined) { + this.headers['content-length'] = String(body.byteLength) + } + this.rawHeaders = rawHeaders(this.headers) + this.once('end', () => { + this.complete = true + }) + this.body = body + } + + private readonly body: Buffer + + override _read(): void { + if (this.sent) return + this.sent = true + if (this.body.length > 0) this.push(this.body) + this.push(null) + } +} + +class MockClientRequest extends Writable { + readonly method: string + readonly path: string + readonly protocol: string + readonly host: string + aborted = false + private readonly headers = new Map() + private responseTimer?: ReturnType + + constructor( + url: URL, + method: string, + headers: RequestOptions['headers'], + private readonly mockedResponse: LegacyMockResponse + ) { + super() + this.method = method + this.path = `${url.pathname}${url.search}` + this.protocol = url.protocol + this.host = url.host + for (const [name, value] of Object.entries(headers ?? {})) { + if (value !== undefined) this.setHeader(name, value) + } + if (!this.hasHeader('host')) this.setHeader('host', url.host) + } + + override _write( + _chunk: Buffer, + _encoding: BufferEncoding, + callback: (error?: Error | null) => void + ): void { + callback() + } + + override _final(callback: (error?: Error | null) => void): void { + const respond = () => { + if (!this.destroyed && !this.aborted) { + this.emit( + 'response', + new MockIncomingMessage(this.mockedResponse) as unknown as IncomingMessage + ) + } + callback() + } + const delay = Math.max(0, this.mockedResponse.delayMs ?? 0) + if (delay === 0) queueMicrotask(respond) + else this.responseTimer = setTimeout(respond, delay) + } + + setHeader(name: string, value: string | string[] | number): this { + this.headers.set(name.toLowerCase(), { name, value }) + return this + } + + getHeader(name: string): string | string[] | number | undefined { + return this.headers.get(name.toLowerCase())?.value + } + + getHeaders(): Record { + return Object.fromEntries( + [...this.headers.entries()].map(([normalized, entry]) => [normalized, entry.value]) + ) + } + + hasHeader(name: string): boolean { + return this.headers.has(name.toLowerCase()) + } + + removeHeader(name: string): void { + this.headers.delete(name.toLowerCase()) + } + + flushHeaders(): void {} + + abort(): void { + if (this.aborted) return + this.aborted = true + if (this.responseTimer) clearTimeout(this.responseTimer) + this.emit('abort') + this.destroy() + } + + setTimeout(_timeout: number, callback?: () => void): this { + if (callback) this.once('timeout', callback) + return this + } + + override _destroy(error: Error | null, callback: (error?: Error | null) => void): void { + if (this.responseTimer) clearTimeout(this.responseTimer) + callback(error) + } +} + +function requestUrl( + arg1: RequestOptions | string | URL, + options: RequestOptions | undefined, + isHttps: boolean +): URL { + const base = + typeof arg1 === 'string' || arg1 instanceof URL + ? new URL(arg1) + : new URL( + `${arg1.protocol ?? (isHttps ? 'https:' : 'http:')}//${arg1.hostname ?? arg1.host ?? 'localhost'}${arg1.port === undefined ? '' : `:${arg1.port}`}${arg1.path ?? '/'}` + ) + if (options?.protocol) base.protocol = options.protocol + if (options?.hostname || options?.host) base.hostname = String(options.hostname ?? options.host) + if (options?.port !== undefined) base.port = String(options.port) + if (options?.path) { + const separator = options.path.indexOf('?') + base.pathname = separator === -1 ? options.path : options.path.slice(0, separator) + base.search = separator === -1 ? '' : options.path.slice(separator) + } + return base +} + +/** Route matching `http.request`/`https.request` calls to an in-process response. */ +export function mockableRequestHandler( + actualRequest: RequestFn, + isHttps: boolean, + rules: readonly LegacyMockRule[] +): RequestFn { + return function mockableRequest( + this: unknown, + arg1: RequestOptions | string | URL, + arg2?: RequestOptions | ((response: IncomingMessage) => void), + arg3?: (response: IncomingMessage) => void + ): ClientRequest { + const options = + typeof arg1 === 'string' || arg1 instanceof URL + ? typeof arg2 === 'object' + ? arg2 + : undefined + : arg1 + const callback = typeof arg2 === 'function' ? arg2 : arg3 + const url = requestUrl(arg1, options, isHttps) + const method = String(options?.method ?? 'GET').toUpperCase() + const rule = findLegacyMock(rules, { + url: url.toString(), + method, + headers: (options?.headers ?? {}) as LegacyMockRequest['headers'] + }) + if (!rule) { + const args = + typeof arg1 === 'string' || arg1 instanceof URL + ? typeof arg2 === 'object' + ? [arg1, arg2, arg3] + : [arg1, arg2] + : [arg1, arg2] + return Reflect.apply(actualRequest, this, args) + } + + const request = new MockClientRequest(url, method, options?.headers, rule.response) + if (callback) request.once('response', callback) + return request as unknown as ClientRequest + } as RequestFn +} + +function fetchHeaders( + request: string | URL | Request, + options?: RequestInit +): Record { + const headers = new Headers(request instanceof Request ? request.headers : undefined) + if (options?.headers) { + new Headers(options.headers).forEach((value, name) => headers.set(name, value)) + } + const result: Record = {} + headers.forEach((value, name) => (result[name] = value)) + return result +} + +export function findFetchMock( + rules: readonly LegacyMockRule[], + request: string | URL | Request, + options?: RequestInit +): LegacyMockRule | undefined { + return findLegacyMock(rules, { + url: + typeof request === 'string' + ? request + : request instanceof URL + ? request.toString() + : request.url, + method: String(options?.method ?? (request instanceof Request ? request.method : 'GET')), + headers: fetchHeaders(request, options) + }) +} + +export async function mockedFetchResponse( + rule: LegacyMockRule, + signal?: AbortSignal | null +): Promise { + const delay = Math.max(0, rule.response.delayMs ?? 0) + if (delay > 0) { + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, delay) + const abort = () => { + clearTimeout(timer) + reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError')) + } + if (signal?.aborted) abort() + else signal?.addEventListener('abort', abort, { once: true }) + }) + } + const status = rule.response.status ?? 200 + const configuredBody = rule.response.bodyBase64 + ? Buffer.from(rule.response.bodyBase64, 'base64') + : (rule.response.body ?? null) + const body = [101, 103, 204, 205, 304].includes(status) ? null : configuredBody + return new Response(body, { + status, + statusText: rule.response.statusText ?? 'Mocked', + headers: rule.response.headers as HeadersInit | undefined + }) +} diff --git a/packages/network-debugger/src/preload/index.test.ts b/packages/network-debugger/src/preload/index.test.ts new file mode 100644 index 0000000..78ed531 --- /dev/null +++ b/packages/network-debugger/src/preload/index.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it, vi } from 'vitest' +import type { ReadyInfo, RegistrationHandle } from '../runtime/registration' +import { NND_PRELOAD_CONFIG_ENV, serializePreloadConfig, type ResolvedNndConfig } from '../config' +import { NND_PRELOAD_PROCESS_ENV, claimPreloadProcess, getPreloadHandle, preload } from './index' + +const resolved: ResolvedNndConfig = { + mode: 'auto', + open: false, + wait: true, + watch: false, + runner: 'node', + inspector: { host: '127.0.0.1', port: 0 }, + requiredCapabilities: [], + legacy: {} +} + +const readyInfo: ReadyInfo = { + mode: 'native', + target: { + id: 'target', + title: 'target', + type: 'node', + url: '', + webSocketDebuggerUrl: 'ws://127.0.0.1:1234/id', + discoveryUrl: 'http://127.0.0.1:1234/json/list' + }, + capabilities: { + http: true, + https: true, + fetch: true, + http2: true, + responseBody: true, + requestBody: false, + websocketLifecycle: false, + websocketFrames: false, + sseMessages: false, + initiator: true + }, + diagnostics: [] +} + +function fakeHandle(ready: Promise = Promise.resolve(readyInfo)): RegistrationHandle { + const handle = (() => undefined) as RegistrationHandle + Object.defineProperties(handle, { + ready: { value: ready }, + status: { value: () => ({ state: 'ready' }) }, + openDevtools: { value: vi.fn() }, + dispose: { value: vi.fn() }, + on: { value: vi.fn() } + }) + return handle +} + +describe('preload', () => { + it('claims only the root process so inherited --import does not register in forks', () => { + const env: NodeJS.ProcessEnv = {} + expect(claimPreloadProcess(env)).toBe(true) + expect(env[NND_PRELOAD_PROCESS_ENV]).toBe('claimed') + expect(claimPreloadProcess(env)).toBe(false) + }) + + it('registers synchronously from serialized CLI config and only once', async () => { + const realm = Object.create(null) as typeof globalThis + const register = vi.fn(() => fakeHandle()) + const env = { [NND_PRELOAD_CONFIG_ENV]: serializePreloadConfig(resolved) } + + const first = preload({ globalObject: realm, env, register }) + const second = preload({ globalObject: realm, env, register }) + + expect(register).toHaveBeenCalledTimes(1) + expect(register).toHaveBeenCalledWith( + expect.objectContaining({ mode: 'auto', devtools: { open: false } }) + ) + expect(second).toBe(first) + const handle = await first + expect(getPreloadHandle(realm)).toBe(handle) + }) + + it('waits for config discovery, registration, and handle readiness', async () => { + const realm = Object.create(null) as typeof globalThis + let releaseConfig!: (value: { + config: ResolvedNndConfig + sources: { env: []; cli: [] } + }) => void + const configPromise = new Promise<{ config: ResolvedNndConfig; sources: { env: []; cli: [] } }>( + (resolvePromise) => (releaseConfig = resolvePromise) + ) + let releaseReady!: (value: ReadyInfo) => void + const ready = new Promise((resolvePromise) => (releaseReady = resolvePromise)) + const register = vi.fn(() => fakeHandle(ready)) + const resolution = preload({ + globalObject: realm, + env: {}, + register, + resolve: vi.fn(() => configPromise) + }) + let completed = false + void resolution.then(() => (completed = true)) + + await Promise.resolve() + expect(register).not.toHaveBeenCalled() + releaseConfig({ config: resolved, sources: { env: [], cli: [] } }) + await Promise.resolve() + expect(register).toHaveBeenCalledTimes(1) + expect(completed).toBe(false) + releaseReady(readyInfo) + await expect(resolution).resolves.toBe(getPreloadHandle(realm)) + expect(completed).toBe(true) + }) + + it('clears failed state so a corrected import can retry', async () => { + const realm = Object.create(null) as typeof globalThis + const register = vi + .fn<() => RegistrationHandle>() + .mockReturnValueOnce(fakeHandle(Promise.reject(new Error('not ready')))) + .mockReturnValueOnce(fakeHandle()) + const env = { [NND_PRELOAD_CONFIG_ENV]: serializePreloadConfig(resolved) } + + await expect(preload({ globalObject: realm, env, register })).rejects.toThrow('not ready') + await expect(preload({ globalObject: realm, env, register })).resolves.toBeDefined() + expect(register).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/network-debugger/src/preload/index.ts b/packages/network-debugger/src/preload/index.ts new file mode 100644 index 0000000..971dfb6 --- /dev/null +++ b/packages/network-debugger/src/preload/index.ts @@ -0,0 +1,119 @@ +import type { RegisterOptions } from '../common' +import { register as runtimeRegister } from '../runtime/controller' +import type { RegistrationHandle } from '../runtime/registration' +import { + NND_PRELOAD_CONFIG_ENV, + parsePreloadConfig, + resolveConfig, + toRegisterOptions, + type ConfigResolution, + type ResolveConfigOptions +} from '../config' + +const PRELOAD_STATE = Symbol.for('node-network-devtools.preload.state') + +interface PreloadState { + promise: Promise + handle?: RegistrationHandle +} + +export interface PreloadDependencies { + env?: NodeJS.ProcessEnv + cwd?: string + globalObject?: typeof globalThis + register?: (options?: RegisterOptions) => RegistrationHandle + resolve?: (options?: ResolveConfigOptions) => Promise +} + +export const NND_PRELOAD_REPORT_ENV = 'NND_PRELOAD_REPORT' +export const NND_READY_PREFIX = '[nnd:ready] ' +export const NND_PRELOAD_PROCESS_ENV = 'NND_PRELOAD_PROCESS' + +/** + * Claim the current OS process as the preload root. Descendants inherit the + * marker and skip the side effect even though Node also inherits `execArgv`. + */ +export function claimPreloadProcess(env: NodeJS.ProcessEnv = process.env): boolean { + if (env[NND_PRELOAD_PROCESS_ENV] === 'claimed') return false + env[NND_PRELOAD_PROCESS_ENV] = 'claimed' + return true +} + +function stateOn(globalObject: typeof globalThis): PreloadState | undefined { + return (globalObject as unknown as Record)[PRELOAD_STATE] as + | PreloadState + | undefined +} + +function setState(globalObject: typeof globalThis, state: PreloadState): void { + Object.defineProperty(globalObject, PRELOAD_STATE, { + value: state, + configurable: true, + enumerable: false, + writable: true + }) +} + +/** + * Register network inspection exactly once for this JavaScript realm. + * + * The CLI serializes its already-resolved configuration into the environment, + * allowing `register()` to run synchronously during module evaluation. Direct + * `--import node-network-devtools/register` usage falls back to config discovery. + */ +export function preload(dependencies: PreloadDependencies = {}): Promise { + const globalObject = dependencies.globalObject ?? globalThis + const existing = stateOn(globalObject) + if (existing) return existing.promise + + const env = dependencies.env ?? process.env + const register = dependencies.register ?? runtimeRegister + const resolve = dependencies.resolve ?? resolveConfig + const state = {} as PreloadState + + const serialized = env[NND_PRELOAD_CONFIG_ENV] + if (serialized) { + try { + const handle = register(toRegisterOptions(parsePreloadConfig(serialized))) + state.handle = handle + state.promise = handle.ready.then(() => handle) + } catch (error) { + state.promise = Promise.reject(error) + } + } else { + state.promise = resolve({ cwd: dependencies.cwd, env }).then(({ config }) => { + const handle = register(toRegisterOptions(config)) + state.handle = handle + return handle.ready.then(() => handle) + }) + } + + // A failed import may be retried after configuration is corrected. + void state.promise.catch(() => { + if (stateOn(globalObject) === state) { + delete (globalObject as unknown as Record)[PRELOAD_STATE] + } + }) + setState(globalObject, state) + return state.promise +} + +export function getPreloadHandle( + globalObject: typeof globalThis = globalThis +): RegistrationHandle | undefined { + return stateOn(globalObject)?.handle +} + +/** Test and embedding hook; normal consumers should dispose the handle instead. */ +export function resetPreloadState(globalObject: typeof globalThis = globalThis): void { + delete (globalObject as unknown as Record)[PRELOAD_STATE] +} + +export function formatPreloadError(error: unknown): string { + const code = + typeof error === 'object' && error !== null && 'code' in error + ? String((error as { code: unknown }).code) + : 'NND_PRELOAD_FAILED' + const message = error instanceof Error ? error.message : String(error) + return `[nnd:${code}] ${message}` +} diff --git a/packages/network-debugger/src/preload/register.test.ts b/packages/network-debugger/src/preload/register.test.ts new file mode 100644 index 0000000..dcc848b --- /dev/null +++ b/packages/network-debugger/src/preload/register.test.ts @@ -0,0 +1,39 @@ +import { expect, it, vi } from 'vitest' +import type { RegistrationHandle } from '../runtime/registration' + +const deferred = vi.hoisted(() => { + let release!: (value: RegistrationHandle) => void + return { + promise: new Promise((resolvePromise) => (release = resolvePromise)), + release + } +}) + +vi.mock('./index', () => ({ + NND_PRELOAD_REPORT_ENV: 'NND_PRELOAD_REPORT', + NND_READY_PREFIX: '[nnd:ready] ', + claimPreloadProcess: () => true, + formatPreloadError: (error: unknown) => String(error), + preload: () => deferred.promise +})) + +it('keeps the side-effect module evaluation pending until preload completes', async () => { + const original = process.env.NND_PRELOAD_REPORT + delete process.env.NND_PRELOAD_REPORT + let imported = false + const importing = import('./register').then(() => { + imported = true + }) + + await Promise.resolve() + await Promise.resolve() + expect(imported).toBe(false) + + const handle = (() => undefined) as RegistrationHandle + deferred.release(handle) + await importing + expect(imported).toBe(true) + + if (original === undefined) delete process.env.NND_PRELOAD_REPORT + else process.env.NND_PRELOAD_REPORT = original +}) diff --git a/packages/network-debugger/src/preload/register.ts b/packages/network-debugger/src/preload/register.ts new file mode 100644 index 0000000..03b1b00 --- /dev/null +++ b/packages/network-debugger/src/preload/register.ts @@ -0,0 +1,29 @@ +import { + NND_PRELOAD_REPORT_ENV, + NND_READY_PREFIX, + claimPreloadProcess, + formatPreloadError, + preload +} from './index' + +// Side-effect entry for `node --import node-network-devtools/register`. +if (claimPreloadProcess()) { + try { + const registration = await preload() + if (process.env[NND_PRELOAD_REPORT_ENV] === '1') { + const ready = await registration.ready + process.stderr.write( + `${NND_READY_PREFIX}${JSON.stringify({ + mode: ready.mode, + target: ready.target, + capabilities: ready.capabilities, + ...(ready.fallbackReason ? { fallbackReason: ready.fallbackReason } : {}) + })}\n` + ) + } + } catch (error) { + process.stderr.write(`${formatPreloadError(error)}\n`) + process.exitCode = 1 + throw error + } +} diff --git a/packages/network-debugger/src/replay/index.test.ts b/packages/network-debugger/src/replay/index.test.ts new file mode 100644 index 0000000..51d38e0 --- /dev/null +++ b/packages/network-debugger/src/replay/index.test.ts @@ -0,0 +1,175 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, test, vi } from 'vitest' +import type { HarDocument, SessionManifest } from '../session/types' +import { replay, sanitizeReplayHeaders } from '.' + +const temporaryDirectories: string[] = [] + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +function har(entries: HarDocument['log']['entries']): HarDocument { + return { + log: { + version: '1.2', + creator: { name: 'node-network-devtools', version: '2.0.0' }, + pages: [], + entries + } + } +} + +function entry(url: string, method = 'POST'): HarDocument['log']['entries'][number] { + return { + startedDateTime: new Date(0).toISOString(), + time: 1, + request: { + method, + url, + httpVersion: 'HTTP/1.1', + cookies: [], + headers: [ + { name: 'Connection', value: 'keep-alive, x-private-hop' }, + { name: 'X-Private-Hop', value: 'remove-me' }, + { name: 'Content-Length', value: '999' }, + { name: 'X-End-To-End', value: 'preserve-me' } + ], + queryString: [], + postData: { mimeType: 'text/plain', text: 'payload' }, + headersSize: -1, + bodySize: 7 + }, + response: { + status: 200, + statusText: 'OK', + httpVersion: 'HTTP/1.1', + cookies: [], + headers: [], + content: { size: 0, mimeType: 'text/plain' }, + redirectURL: '', + headersSize: -1, + bodySize: 0 + }, + cache: {}, + timings: { blocked: -1, dns: -1, connect: -1, send: 0, wait: 1, receive: 0, ssl: -1 }, + _requestId: 'request-1' + } +} + +describe('replay', () => { + test('sanitizes hop-by-hop, Connection-nominated, and runtime-owned headers', () => { + expect( + sanitizeReplayHeaders([ + ['Connection', 'X-Temporary'], + ['X-Temporary', 'no'], + ['Transfer-Encoding', 'chunked'], + ['Host', 'old.example'], + ['Content-Length', '10'], + ['Traceparent', '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01'] + ]) + ).toEqual({ + Traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' + }) + }) + + test('builds a deterministic HAR dry-run without opening the network', async () => { + const fetch = vi.fn() + const report = await replay(har([entry('http://127.0.0.1:43101/replay')]), { + dryRun: true, + fetch: fetch as unknown as typeof globalThis.fetch + }) + + expect(fetch).not.toHaveBeenCalled() + expect(report).toMatchObject({ dryRun: true, succeeded: 1, failed: 0 }) + expect(report.requests).toEqual([ + { + index: 0, + requestId: 'request-1', + method: 'POST', + url: 'http://127.0.0.1:43101/replay', + headers: { 'X-End-To-End': 'preserve-me' }, + body: 'payload' + } + ]) + }) + + test('loads a Session directory in event sequence order', async () => { + const directory = await mkdtemp(join(tmpdir(), 'nnd-replay-session-')) + temporaryDirectories.push(directory) + const manifest: SessionManifest = { + schemaVersion: 1, + sessionId: 'session-1', + state: 'completed', + createdAt: new Date(0).toISOString(), + completedAt: new Date(1).toISOString(), + target: { + id: 'target', + title: 'target', + type: 'node', + url: '', + webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/target', + discoveryUrl: 'http://127.0.0.1/json/list' + }, + files: { events: 'events.ndjson', bodies: 'bodies' }, + stats: { + eventCount: 2, + requestCount: 2, + bodyCount: 0, + bodyErrorCount: 0, + failedRequestCount: 0 + }, + requestIndex: { + second: { + requestId: 'second', + firstSequence: 2, + request: { url: 'http://127.0.0.1/second', method: 'GET', headers: {} } + }, + first: { + requestId: 'first', + firstSequence: 1, + request: { + url: 'http://127.0.0.1/first', + method: 'POST', + headers: { 'content-type': 'text/plain' }, + postData: 'first-body' + } + } + }, + bodyIndex: {}, + traceIndex: {}, + issues: [] + } + await writeFile(join(directory, 'manifest.json'), JSON.stringify(manifest)) + + const report = await replay(directory, { dryRun: true }) + expect(report.requests.map((request) => request.requestId)).toEqual(['first', 'second']) + expect(report.requests[0].body).toBe('first-body') + expect(JSON.parse(await readFile(join(directory, 'manifest.json'), 'utf8'))).toEqual(manifest) + }) + + test('performs real fetch calls, drains bodies, and reports HTTP failures', async () => { + const responseBody = vi.fn(async () => new ArrayBuffer(0)) + const fetch = vi.fn(async () => ({ + ok: false, + status: 503, + statusText: 'Unavailable', + headers: new Headers({ 'x-result': 'captured' }), + arrayBuffer: responseBody + })) + + const report = await replay(har([entry('http://127.0.0.1/service')]), { + fetch: fetch as unknown as typeof globalThis.fetch + }) + + expect(fetch).toHaveBeenCalledWith( + 'http://127.0.0.1/service', + expect.objectContaining({ method: 'POST', body: 'payload', redirect: 'manual' }) + ) + expect(responseBody).toHaveBeenCalledOnce() + expect(report).toMatchObject({ succeeded: 0, failed: 1 }) + expect(report.results[0]).toMatchObject({ ok: false, status: 503 }) + }) +}) diff --git a/packages/network-debugger/src/replay/index.ts b/packages/network-debugger/src/replay/index.ts new file mode 100644 index 0000000..7a60c0d --- /dev/null +++ b/packages/network-debugger/src/replay/index.ts @@ -0,0 +1,260 @@ +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' +import { readSessionManifest } from '../session/files' +import type { + HarDocument, + HarEntry, + HarHeader, + SessionManifest, + SessionRequestIndexEntry +} from '../session/types' +import type { + ReplayOptions, + ReplayReport, + ReplayRequest, + ReplayResult, + ReplaySource +} from './types' + +export type { + ReplayOptions, + ReplayReport, + ReplayRequest, + ReplayResult, + ReplaySource +} from './types' + +const DEFAULT_TIMEOUT_MS = 30_000 +const HOP_BY_HOP_HEADERS = new Set([ + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade' +]) +const RUNTIME_MANAGED_HEADERS = new Set(['content-length', 'host']) + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function stringValue(value: unknown): string | undefined { + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean') return String(value) + if (Array.isArray(value)) return value.map(String).join(', ') + return undefined +} + +/** Remove RFC hop-by-hop fields, fields named by Connection, and Fetch-owned fields. */ +export function sanitizeReplayHeaders( + headers: Iterable +): Record { + const values = [...headers] + const connectionTokens = new Set() + for (const [name, value] of values) { + if (name.toLowerCase() !== 'connection') continue + for (const token of value.split(',')) { + const normalized = token.trim().toLowerCase() + if (normalized) connectionTokens.add(normalized) + } + } + + const sanitized: Record = {} + for (const [name, value] of values) { + const normalized = name.trim().toLowerCase() + if ( + !normalized || + HOP_BY_HOP_HEADERS.has(normalized) || + RUNTIME_MANAGED_HEADERS.has(normalized) || + connectionTokens.has(normalized) + ) { + continue + } + sanitized[name] = value + } + return sanitized +} + +function headerPairs(value: unknown): Array<[string, string]> { + if (Array.isArray(value)) { + return value.flatMap((header): Array<[string, string]> => { + if (!isRecord(header) || typeof header.name !== 'string') return [] + const stringified = stringValue(header.value) + return stringified === undefined ? [] : [[header.name, stringified]] + }) + } + if (!isRecord(value)) return [] + return Object.entries(value).flatMap(([name, nested]): Array<[string, string]> => { + const stringified = stringValue(nested) + return stringified === undefined ? [] : [[name, stringified]] + }) +} + +function assertReplayUrl(url: string, index: number): void { + let parsed: URL + try { + parsed = new URL(url) + } catch { + throw new Error(`Replay request ${index} has an invalid URL: ${url}`) + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error( + `Replay request ${index} uses unsupported protocol ${parsed.protocol}; only HTTP(S) can be replayed.` + ) + } +} + +function requestFromHar(entry: HarEntry, index: number): ReplayRequest { + const method = entry.request.method.toUpperCase() + const url = entry.request.url + assertReplayUrl(url, index) + const body = method === 'GET' || method === 'HEAD' ? undefined : entry.request.postData?.text + return { + index, + requestId: entry._requestId, + method, + url, + headers: sanitizeReplayHeaders( + entry.request.headers.map((header: HarHeader) => [header.name, header.value] as const) + ), + ...(body !== undefined ? { body } : {}) + } +} + +function requestFromSession( + entry: SessionRequestIndexEntry, + requestId: string, + index: number +): ReplayRequest | undefined { + const request = entry.request + if (!request || typeof request.url !== 'string') return undefined + const method = + typeof request.method === 'string' && request.method ? request.method.toUpperCase() : 'GET' + assertReplayUrl(request.url, index) + const bodyValue = stringValue(request.postData) + return { + index, + requestId, + method, + url: request.url, + headers: sanitizeReplayHeaders(headerPairs(request.headers)), + ...(method !== 'GET' && method !== 'HEAD' && bodyValue !== undefined ? { body: bodyValue } : {}) + } +} + +function requestsFromManifest(manifest: SessionManifest): ReplayRequest[] { + return Object.entries(manifest.requestIndex) + .sort(([, left], [, right]) => left.firstSequence - right.firstSequence) + .flatMap(([requestId, entry], index) => { + const request = requestFromSession(entry, requestId, index) + return request ? [request] : [] + }) + .map((request, index) => ({ ...request, index })) +} + +function assertHar(value: unknown, source: string): HarDocument { + if (!isRecord(value) || !isRecord(value.log) || !Array.isArray(value.log.entries)) { + throw new Error(`Replay source is not a HAR 1.2 document: ${source}`) + } + return value as unknown as HarDocument +} + +/** Load a deterministic replay plan from a Session directory or HAR document/file. */ +export async function loadReplayRequests(source: ReplaySource): Promise { + if (typeof source !== 'string') { + return source.log.entries.map(requestFromHar) + } + + const absolute = resolve(source) + const details = await stat(absolute) + if (details.isDirectory()) { + return requestsFromManifest(await readSessionManifest(absolute)) + } + if (!details.isFile()) throw new Error(`Replay source is not a file or directory: ${absolute}`) + const har = assertHar(JSON.parse(await readFile(absolute, 'utf8')), absolute) + return har.log.entries.map(requestFromHar) +} + +function timeoutValue(value: number | undefined): number { + return Number.isFinite(value) && Number(value) > 0 ? Number(value) : DEFAULT_TIMEOUT_MS +} + +function headersToRecord(headers: Headers): Record { + const values: Record = {} + headers.forEach((value, name) => { + values[name] = value + }) + return values +} + +/** Replay HTTP(S) requests, or return the exact request plan in dry-run mode. */ +export async function replay( + source: ReplaySource, + options: ReplayOptions = {} +): Promise { + const startedAt = new Date().toISOString() + const requests = await loadReplayRequests(source) + const results: ReplayResult[] = [] + const dryRun = options.dryRun === true + const fetchImplementation = options.fetch ?? globalThis.fetch + if (!dryRun && typeof fetchImplementation !== 'function') { + throw new Error('Replay requires a Fetch implementation on this Node.js runtime.') + } + + for (const request of requests) { + const started = performance.now() + if (dryRun) { + results.push({ request, dryRun: true, ok: true, durationMs: 0 }) + continue + } + + const abortController = new AbortController() + const timer = setTimeout(() => abortController.abort(), timeoutValue(options.timeoutMs)) + timer.unref?.() + try { + const response = await fetchImplementation(request.url, { + method: request.method, + headers: request.headers, + ...(request.body !== undefined ? { body: request.body } : {}), + redirect: 'manual', + signal: abortController.signal + }) + await response.arrayBuffer() + results.push({ + request, + dryRun: false, + ok: response.ok, + status: response.status, + statusText: response.statusText, + responseHeaders: headersToRecord(response.headers), + durationMs: performance.now() - started + }) + if (!response.ok && options.stopOnError) break + } catch (error) { + results.push({ + request, + dryRun: false, + ok: false, + error: error instanceof Error ? error.message : String(error), + durationMs: performance.now() - started + }) + if (options.stopOnError) break + } finally { + clearTimeout(timer) + } + } + + const succeeded = results.filter((result) => result.ok).length + return { + dryRun, + startedAt, + completedAt: new Date().toISOString(), + requests, + results, + succeeded, + failed: results.length - succeeded + } +} diff --git a/packages/network-debugger/src/replay/types.ts b/packages/network-debugger/src/replay/types.ts new file mode 100644 index 0000000..9a6a3a0 --- /dev/null +++ b/packages/network-debugger/src/replay/types.ts @@ -0,0 +1,44 @@ +import type { HarDocument } from '../session/types' + +export type ReplaySource = string | HarDocument + +export interface ReplayRequest { + index: number + requestId?: string + method: string + url: string + headers: Record + body?: string +} + +export interface ReplayOptions { + /** Resolve and validate requests without opening a network connection. */ + dryRun?: boolean + /** Stop after the first failed request. Defaults to false. */ + stopOnError?: boolean + /** Per-request timeout. Defaults to 30 seconds. */ + timeoutMs?: number + /** Test/embedding hook. The global Fetch implementation is used by default. */ + fetch?: typeof fetch +} + +export interface ReplayResult { + request: ReplayRequest + dryRun: boolean + ok: boolean + status?: number + statusText?: string + responseHeaders?: Record + error?: string + durationMs: number +} + +export interface ReplayReport { + dryRun: boolean + startedAt: string + completedAt: string + requests: ReplayRequest[] + results: ReplayResult[] + succeeded: number + failed: number +} diff --git a/packages/network-debugger/src/runtime/controller.ts b/packages/network-debugger/src/runtime/controller.ts new file mode 100644 index 0000000..55a4e66 --- /dev/null +++ b/packages/network-debugger/src/runtime/controller.ts @@ -0,0 +1,376 @@ +import type { RegisterOptions } from '../common' +import { LegacyAdapter } from '../adapters/legacy' +import { NodeNativeAdapter, NodeNativeAdapterError } from '../adapters/node-native' +import { AdapterSelectionError, AdapterSelector } from '../adapters/selector' +import type { AdapterMode, Diagnostic, NetworkCapability } from '../adapters/types' +import { openDevtoolsTarget } from '../target/frontend-launcher' +import { exportHar, SessionRecorder } from '../session' +import { + createRegistrationHandle, + type RegistrationHandle, + type RuntimeAdapterSession +} from './registration' + +export class RuntimeRegistrationError extends Error { + constructor( + readonly code: + | 'NND_ALREADY_REGISTERED' + | 'NND_INVALID_MODE' + | 'NND_INVALID_SESSION_OPTIONS' + | 'NND_NATIVE_MOCK_CONFLICT', + message: string, + readonly details: Readonly> = {} + ) { + super(message) + this.name = 'RuntimeRegistrationError' + } +} + +interface NormalizedOptions { + mode: AdapterMode + requiredCapabilities: readonly NetworkCapability[] + inspector: { + host: string + port: number + } + devtools: { + open: boolean + } + session?: NonNullable + legacy: { + port?: number + serverPort?: number + intercept?: RegisterOptions['intercept'] + mock: NonNullable['mock']> + } + diagnostics: readonly Diagnostic[] +} + +interface ActiveRegistration { + key: string + handle: RegistrationHandle +} + +let activeRegistration: ActiveRegistration | undefined + +function normalizeOptions(options: RegisterOptions = {}): NormalizedOptions { + const diagnostics: Diagnostic[] = [] + const mode = options.mode ?? options.adapter ?? 'auto' + if (!['auto', 'native', 'legacy'].includes(mode)) { + throw new RuntimeRegistrationError('NND_INVALID_MODE', `Unknown adapter mode: ${mode}.`, { + mode + }) + } + + const mock = options.legacy?.mock ?? [] + if (mode === 'native' && mock.length > 0) { + throw new RuntimeRegistrationError( + 'NND_NATIVE_MOCK_CONFLICT', + 'Request/response mocking is available only with the Legacy backend.', + { + mode, + mockRuleCount: mock.length, + hint: 'Use mode: "legacy" or mode: "auto" when legacy.mock rules are configured.' + } + ) + } + + if (options.session) { + if (typeof options.session.directory !== 'string' || !options.session.directory.trim()) { + throw new RuntimeRegistrationError( + 'NND_INVALID_SESSION_OPTIONS', + 'Session recording requires a non-empty directory.' + ) + } + if ( + options.session.bodyCommandTimeoutMs !== undefined && + (!Number.isSafeInteger(options.session.bodyCommandTimeoutMs) || + options.session.bodyCommandTimeoutMs <= 0) + ) { + throw new RuntimeRegistrationError( + 'NND_INVALID_SESSION_OPTIONS', + 'session.bodyCommandTimeoutMs must be a positive integer.' + ) + } + if (typeof options.session.har === 'string' && !options.session.har.trim()) { + throw new RuntimeRegistrationError( + 'NND_INVALID_SESSION_OPTIONS', + 'session.har must be true, false, or a non-empty output path.' + ) + } + } + + if (options.adapter !== undefined) { + diagnostics.push({ + code: 'NND_OPTION_ADAPTER_DEPRECATED', + level: 'warn', + message: 'The "adapter" option is deprecated; use "mode".', + hint: `Replace adapter: "${options.adapter}" with mode: "${options.adapter}".` + }) + } + + const usesLegacyTopLevelOptions = + options.port !== undefined || + options.serverPort !== undefined || + options.intercept !== undefined || + options.autoOpenDevtool !== undefined + if (usesLegacyTopLevelOptions) { + diagnostics.push({ + code: 'NND_LEGACY_OPTIONS_DEPRECATED', + level: 'warn', + message: 'Top-level Legacy options are deprecated.', + hint: 'Move capture and port settings under "legacy", and browser behavior under "devtools".' + }) + } + + return { + mode, + requiredCapabilities: options.requiredCapabilities ?? options.requiredFeatures ?? [], + inspector: { + host: options.inspector?.host ?? '127.0.0.1', + port: options.inspector?.port ?? 0 + }, + devtools: { + open: options.devtools?.open ?? options.autoOpenDevtool ?? false + }, + ...(options.session ? { session: { ...options.session } } : {}), + legacy: { + port: options.legacy?.port ?? options.port, + serverPort: options.legacy?.serverPort ?? options.serverPort, + intercept: options.legacy?.intercept ?? options.intercept, + mock + }, + diagnostics + } +} + +function configurationKey(options: NormalizedOptions): string { + return JSON.stringify({ + mode: options.mode, + requiredCapabilities: [...options.requiredCapabilities].sort(), + inspector: options.inspector, + devtools: options.devtools, + session: options.session, + legacy: options.legacy + }) +} + +function uniqueDiagnostics(diagnostics: readonly Diagnostic[]): readonly Diagnostic[] { + const seen = new Set() + return diagnostics.filter((diagnostic) => { + const key = `${diagnostic.code}:${diagnostic.message}` + if (seen.has(key)) return false + seen.add(key) + return true + }) +} + +function promoteForcedNativeError(error: unknown): unknown { + if (!(error instanceof AdapterSelectionError)) return error + const diagnostics = error.details.diagnostics + if (!Array.isArray(diagnostics)) return error + const nativeDiagnostics = diagnostics.filter( + (diagnostic): diagnostic is Diagnostic => + typeof diagnostic === 'object' && + diagnostic !== null && + typeof (diagnostic as Diagnostic).code === 'string' && + typeof (diagnostic as Diagnostic).message === 'string' + ) + const actionable = nativeDiagnostics.find((diagnostic) => diagnostic.level === 'error') + return actionable ? new NodeNativeAdapterError(actionable, nativeDiagnostics) : error +} + +async function attachSessionRecorder( + session: RuntimeAdapterSession, + options: NormalizedOptions['session'] +): Promise { + if (!options) return session + + let recorder: SessionRecorder + try { + recorder = await SessionRecorder.start({ + directory: options.directory, + target: session.target, + bodyCommandTimeoutMs: options.bodyCommandTimeoutMs + }) + } catch (error) { + await session.dispose().catch(() => undefined) + throw error + } + + const manifest = recorder.getManifest() + let disposePromise: Promise | undefined + return { + ...session, + diagnostics: uniqueDiagnostics([ + ...session.diagnostics, + { + code: 'NND_SESSION_RECORDING_STARTED', + level: 'info', + message: `Recording Network events to ${recorder.directory}.`, + details: { directory: recorder.directory, sessionId: manifest.sessionId } + } + ]), + recording: { directory: recorder.directory, sessionId: manifest.sessionId }, + dispose() { + if (disposePromise) return disposePromise + disposePromise = (async () => { + let recordingError: unknown + try { + await recorder.close() + if (options.har) { + await exportHar( + recorder.directory, + typeof options.har === 'string' ? options.har : undefined + ) + } + } catch (error) { + recordingError = error + } + + let backendError: unknown + try { + await session.dispose() + } catch (error) { + backendError = error + } + if (recordingError) throw recordingError + if (backendError) throw backendError + })() + return disposePromise + } + } +} + +function startRuntime(options: NormalizedOptions): Promise { + const native = new NodeNativeAdapter() + const legacy = new LegacyAdapter({ + port: options.legacy.port, + serverPort: options.legacy.serverPort, + // Browser launching is owned by the registration handle, never Legacy. + autoOpenDevtool: false, + intercept: options.legacy.intercept, + mock: options.legacy.mock + }) + const mockFallbackReason: Diagnostic | undefined = + options.mode === 'auto' && options.legacy.mock.length > 0 + ? { + code: 'NND_AUTO_LEGACY_MOCK_REQUIRED', + level: 'info', + message: 'Auto selected Legacy because request/response mocking was configured.', + hint: 'Remove legacy.mock to allow Auto to select the Native backend.' + } + : undefined + const selectionMode = mockFallbackReason ? 'legacy' : options.mode + const selectionOptions = { + mode: selectionMode, + requiredCapabilities: options.requiredCapabilities, + inspector: options.inspector + } as const + const startOptions = { + requiredCapabilities: options.requiredCapabilities, + inspector: options.inspector + } + const selector = new AdapterSelector([native, legacy]) + + // Actual adapter probes are deliberately synchronous and side-effect-free. + // Start the predicted adapter before the first Promise turn so the historical + // `register(); request()` form cannot miss its first Legacy request. The + // selector remains authoritative and verifies the prediction asynchronously. + const nativeProbe = native.probe(startOptions) + const legacyProbe = legacy.probe(startOptions) + const hasRequirements = (capabilities: typeof nativeProbe.capabilities) => + options.requiredCapabilities.every((capability) => capabilities[capability]) + const predicted = + selectionMode === 'native' + ? nativeProbe.available && hasRequirements(nativeProbe.capabilities) + ? native + : undefined + : selectionMode === 'legacy' + ? legacyProbe.available && hasRequirements(legacyProbe.capabilities) + ? legacy + : undefined + : nativeProbe.available && + nativeProbe.autoSelectable !== false && + hasRequirements(nativeProbe.capabilities) + ? native + : legacyProbe.available && + legacyProbe.autoSelectable !== false && + hasRequirements(legacyProbe.capabilities) + ? legacy + : undefined + const startedSession = predicted?.start(startOptions) + + return selector + .select(selectionOptions) + .then(async (selection) => { + let session + if (predicted === selection.adapter && startedSession) { + session = await startedSession + } else { + if (startedSession) await (await startedSession).dispose() + session = await selection.adapter.start(startOptions) + } + const diagnostics = uniqueDiagnostics([ + ...options.diagnostics, + ...session.diagnostics, + ...(selection.fallbackReason ? [selection.fallbackReason] : []), + ...(mockFallbackReason ? [mockFallbackReason] : []) + ]) + + return attachSessionRecorder( + { + ...session, + diagnostics, + fallbackReason: mockFallbackReason ?? selection.fallbackReason + }, + options.session + ) + }) + .catch(async (error) => { + if (startedSession) await (await startedSession).dispose() + throw options.mode === 'native' ? promoteForcedNativeError(error) : error + }) +} + +/** + * Register network inspection for this process. + * + * The return value is intentionally callable for v1 compatibility while also + * exposing an observable asynchronous lifecycle for v2 consumers. + */ +export function register(options: RegisterOptions = {}): RegistrationHandle { + const normalized = normalizeOptions(options) + const key = configurationKey(normalized) + + if (activeRegistration) { + if (activeRegistration.key === key) return activeRegistration.handle + throw new RuntimeRegistrationError( + 'NND_ALREADY_REGISTERED', + 'Node Network Devtools is already registered with a different configuration.', + { activeConfiguration: activeRegistration.key, requestedConfiguration: key } + ) + } + + const session = startRuntime(normalized) + let handle!: RegistrationHandle + handle = createRegistrationHandle({ + session, + openTarget: openDevtoolsTarget, + openOnReady: normalized.devtools.open, + onDisposed: () => { + if (activeRegistration?.handle === handle) activeRegistration = undefined + } + }) + activeRegistration = { key, handle } + + void handle.ready.catch(() => { + if (activeRegistration?.handle === handle) activeRegistration = undefined + }) + return handle +} + +/** Test-only reset for isolated module suites. */ +export async function disposeActiveRegistration(): Promise { + await activeRegistration?.handle.dispose() +} diff --git a/packages/network-debugger/src/runtime/registration.test.ts b/packages/network-debugger/src/runtime/registration.test.ts new file mode 100644 index 0000000..a983775 --- /dev/null +++ b/packages/network-debugger/src/runtime/registration.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test, vi } from 'vitest' +import type { RuntimeAdapterSession } from './registration' +import { createRegistrationHandle } from './registration' + +function session(overrides: Partial = {}): RuntimeAdapterSession { + return { + kind: 'native', + capabilities: { + http: true, + https: true, + fetch: true, + http2: true, + responseBody: true, + requestBody: false, + websocketLifecycle: true, + websocketFrames: false, + sseMessages: false, + initiator: true + }, + target: { + id: 'target', + title: 'Node.js', + type: 'node', + url: 'file:///app.mjs', + webSocketDebuggerUrl: 'ws://127.0.0.1:9229/target', + devtoolsFrontendUrl: 'devtools://target', + discoveryUrl: 'http://127.0.0.1:9229/json/list' + }, + diagnostics: [], + dispose: vi.fn().mockResolvedValue(undefined), + ...overrides + } +} + +describe('RegistrationHandle', () => { + test('emits typed readiness and diagnostics, then disposes once', async () => { + const diagnostic = { + code: 'NND_AUTO_FALLBACK', + level: 'warn' as const, + message: 'Using Legacy.' + } + const adapterSession = session({ kind: 'legacy', diagnostics: [diagnostic] }) + const stateListener = vi.fn() + const diagnosticListener = vi.fn() + const onDisposed = vi.fn() + const handle = createRegistrationHandle({ + session: Promise.resolve(adapterSession), + openTarget: vi.fn(), + onDisposed + }) + + handle.on('state', stateListener) + handle.on('diagnostic', diagnosticListener) + const ready = await handle.ready + + expect(ready.mode).toBe('legacy') + expect(stateListener).toHaveBeenCalledWith({ state: 'ready', mode: 'legacy' }) + expect(diagnosticListener).toHaveBeenCalledWith(diagnostic) + + await Promise.all([handle.dispose(), handle.dispose()]) + expect(adapterSession.dispose).toHaveBeenCalledOnce() + expect(onDisposed).toHaveBeenCalledOnce() + }) + + test('does not launch a frontend until explicitly requested', async () => { + const openTarget = vi.fn().mockResolvedValue(undefined) + const adapterSession = session() + const handle = createRegistrationHandle({ + session: Promise.resolve(adapterSession), + openTarget + }) + + await handle.ready + expect(openTarget).not.toHaveBeenCalled() + + await handle.openDevtools() + expect(openTarget).toHaveBeenCalledWith(adapterSession.target) + }) + + test('supports opt-in opening without coupling disposal to the frontend', async () => { + const openTarget = vi.fn().mockResolvedValue(undefined) + const adapterSession = session() + const handle = createRegistrationHandle({ + session: Promise.resolve(adapterSession), + openTarget, + openOnReady: true + }) + + await handle.ready + expect(openTarget).toHaveBeenCalledOnce() + await handle.dispose() + expect(openTarget).toHaveBeenCalledOnce() + expect(adapterSession.dispose).toHaveBeenCalledOnce() + }) + + test('surfaces a terminal backend failure after initial readiness', async () => { + let fail!: (error: Error) => void + const unsubscribe = vi.fn() + const adapterSession = session({ + kind: 'legacy', + onFailure(listener) { + fail = listener + return unsubscribe + } + }) + const stateListener = vi.fn() + const handle = createRegistrationHandle({ + session: Promise.resolve(adapterSession), + openTarget: vi.fn() + }) + handle.on('state', stateListener) + + await handle.ready + const error = new Error('Legacy recovery exhausted') + fail(error) + + expect(handle.status()).toEqual({ state: 'failed', mode: 'legacy', error }) + expect(stateListener).toHaveBeenLastCalledWith({ + state: 'failed', + mode: 'legacy', + error + }) + await handle.dispose() + expect(adapterSession.dispose).toHaveBeenCalledOnce() + expect(unsubscribe).toHaveBeenCalledOnce() + }) +}) diff --git a/packages/network-debugger/src/runtime/registration.ts b/packages/network-debugger/src/runtime/registration.ts new file mode 100644 index 0000000..6de2bf1 --- /dev/null +++ b/packages/network-debugger/src/runtime/registration.ts @@ -0,0 +1,166 @@ +import type { + AdapterKind, + AdapterSession, + CapabilityMap, + DevtoolsTarget, + Diagnostic +} from '../adapters/types' + +export type RegistrationState = 'starting' | 'ready' | 'disposing' | 'disposed' | 'failed' + +export interface ReadyInfo { + mode: AdapterKind + target: DevtoolsTarget + capabilities: CapabilityMap + diagnostics: readonly Diagnostic[] + fallbackReason?: Diagnostic + session?: { + directory: string + sessionId: string + } +} + +export interface RegistrationStatus { + state: RegistrationState + mode?: AdapterKind + error?: unknown +} + +export type RegistrationEvent = + | { type: 'state'; status: RegistrationStatus } + | { type: 'diagnostic'; diagnostic: Diagnostic } + +export interface RuntimeAdapterSession extends AdapterSession { + fallbackReason?: Diagnostic + recording?: ReadyInfo['session'] +} + +export type RegistrationHandle = (() => void) & { + readonly ready: Promise + status(): RegistrationStatus + openDevtools(): Promise + dispose(): Promise + on(listener: (event: RegistrationEvent) => void): () => void + on(type: 'state', listener: (status: RegistrationStatus) => void): () => void + on(type: 'diagnostic', listener: (diagnostic: Diagnostic) => void): () => void +} + +interface RegistrationHandleOptions { + session: Promise + openTarget(target: DevtoolsTarget): Promise + openOnReady?: boolean + onDisposed?(): void +} + +export function createRegistrationHandle(options: RegistrationHandleOptions): RegistrationHandle { + let status: RegistrationStatus = { state: 'starting' } + let adapterSession: RuntimeAdapterSession | undefined + let unsubscribeSessionDiagnostics: (() => void) | undefined + let unsubscribeSessionFailure: (() => void) | undefined + let disposePromise: Promise | undefined + const listeners = new Set<(event: RegistrationEvent) => void>() + + const emit = (event: RegistrationEvent) => { + for (const listener of listeners) listener(event) + } + const updateStatus = (next: RegistrationStatus) => { + status = next + emit({ type: 'state', status: { ...next } }) + } + + const ready = options.session.then( + async (session) => { + adapterSession = session + const info: ReadyInfo = { + mode: session.kind, + target: session.target, + capabilities: session.capabilities, + diagnostics: session.diagnostics, + fallbackReason: session.fallbackReason, + ...(session.recording ? { session: { ...session.recording } } : {}) + } + updateStatus({ state: 'ready', mode: session.kind }) + for (const diagnostic of session.diagnostics) { + emit({ type: 'diagnostic', diagnostic }) + } + unsubscribeSessionDiagnostics = session.onDiagnostic?.((diagnostic) => { + emit({ type: 'diagnostic', diagnostic }) + }) + unsubscribeSessionFailure = session.onFailure?.((error) => { + if (status.state === 'ready') { + updateStatus({ state: 'failed', mode: session.kind, error }) + } + }) + if (options.openOnReady) await options.openTarget(session.target) + return info + }, + (error) => { + updateStatus({ state: 'failed', error }) + throw error + } + ) + + const dispose = () => { + if (disposePromise) return disposePromise + disposePromise = (async () => { + if (status.state !== 'failed') { + updateStatus({ state: 'disposing', mode: adapterSession?.kind }) + } + try { + const session = adapterSession ?? (await options.session) + await session.dispose() + } catch (error) { + if (status.state !== 'failed') throw error + } finally { + unsubscribeSessionDiagnostics?.() + unsubscribeSessionDiagnostics = undefined + unsubscribeSessionFailure?.() + unsubscribeSessionFailure = undefined + updateStatus({ state: 'disposed', mode: adapterSession?.kind }) + listeners.clear() + options.onDisposed?.() + } + })() + return disposePromise + } + + const callable = (() => { + void dispose() + }) as RegistrationHandle + + Object.defineProperties(callable, { + ready: { value: ready, enumerable: true }, + status: { value: () => ({ ...status }), enumerable: true }, + openDevtools: { + value: async () => { + const info = await ready + await options.openTarget(info.target) + }, + enumerable: true + }, + dispose: { value: dispose, enumerable: true }, + on: { + value: ( + typeOrListener: RegistrationEvent['type'] | ((event: RegistrationEvent) => void), + typedListener?: ((value: RegistrationStatus) => void) | ((value: Diagnostic) => void) + ) => { + const listener = + typeof typeOrListener === 'function' + ? typeOrListener + : (event: RegistrationEvent) => { + if (event.type !== typeOrListener) return + if (event.type === 'state') { + ;(typedListener as (status: RegistrationStatus) => void)?.(event.status) + } else { + ;(typedListener as (diagnostic: Diagnostic) => void)?.(event.diagnostic) + } + } + listeners.add(listener) + return () => listeners.delete(listener) + }, + enumerable: true + } + }) + + return callable +} diff --git a/packages/network-debugger/src/session/files.ts b/packages/network-debugger/src/session/files.ts new file mode 100644 index 0000000..165c8f9 --- /dev/null +++ b/packages/network-debugger/src/session/files.ts @@ -0,0 +1,60 @@ +import { createHash, randomUUID } from 'node:crypto' +import { promises as fs } from 'node:fs' +import { dirname, isAbsolute, relative, resolve, sep } from 'node:path' +import { SESSION_SCHEMA_VERSION, type SessionManifest } from './types' + +export function sha256(value: string | Buffer): string { + return createHash('sha256').update(value).digest('hex') +} + +export function bodyFileName(requestId: string, body: Buffer): string { + return `${sha256(requestId).slice(0, 16)}-${sha256(body)}.body` +} + +export function jsonStringify(value: unknown, spacing?: number): string { + return JSON.stringify( + value, + (_key, nested) => (typeof nested === 'bigint' ? nested.toString() : nested), + spacing + ) +} + +export async function atomicWriteJson(path: string, value: unknown): Promise { + await fs.mkdir(dirname(path), { recursive: true }) + const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp` + try { + await fs.writeFile(temporaryPath, `${jsonStringify(value, 2)}\n`, { + encoding: 'utf8', + flag: 'wx' + }) + await fs.rename(temporaryPath, path) + } catch (error) { + await fs.unlink(temporaryPath).catch(() => undefined) + throw error + } +} + +export function resolveInside(rootDirectory: string, relativePath: string): string { + if (isAbsolute(relativePath)) throw new Error(`Session path must be relative: ${relativePath}`) + const root = resolve(rootDirectory) + const absolute = resolve(root, relativePath) + const pathFromRoot = relative(root, absolute) + if (pathFromRoot === '' || pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`)) { + throw new Error(`Session path escapes its root: ${relativePath}`) + } + return absolute +} + +export async function readSessionManifest(directory: string): Promise { + const manifestPath = resolve(directory, 'manifest.json') + const parsed = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as Partial + if (parsed.schemaVersion !== SESSION_SCHEMA_VERSION) { + throw new Error( + `Unsupported session schema version ${String(parsed.schemaVersion)} in ${manifestPath}.` + ) + } + if (!parsed.target || !parsed.requestIndex || !parsed.bodyIndex || !parsed.traceIndex) { + throw new Error(`Invalid session manifest: ${manifestPath}.`) + } + return parsed as SessionManifest +} diff --git a/packages/network-debugger/src/session/har.test.ts b/packages/network-debugger/src/session/har.test.ts new file mode 100644 index 0000000..0d5bfaf --- /dev/null +++ b/packages/network-debugger/src/session/har.test.ts @@ -0,0 +1,245 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { afterEach, describe, expect, test } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import { buildHar, exportHar } from './har' +import { readSessionManifest } from './files' +import { SessionRecorder } from './recorder' +import type { CdpProtocolEvent, ResponseBodyResult, SessionProtocolConnection } from './types' + +const TARGET: DevtoolsTarget = { + id: 'har-test', + title: 'HAR test', + type: 'node', + url: '', + webSocketDebuggerUrl: 'ws://127.0.0.1:43124/devtools/page/har-test', + discoveryUrl: 'http://127.0.0.1:43124/json/list' +} + +class HarTap implements SessionProtocolConnection { + readonly target = TARGET + state: SessionProtocolConnection['state'] = 'idle' + readonly bodies = new Map() + private readonly listeners = new Set<(event: CdpProtocolEvent) => void | Promise>() + + async connect() { + this.state = 'open' + } + + async command(_method: string, params: Record = {}) { + const result = this.bodies.get(String(params.requestId)) + if (!result) throw new Error('missing body') + return result as T + } + + onEvent(listener: (event: CdpProtocolEvent) => void | Promise) { + this.listeners.add(listener) + return () => this.listeners.delete(listener) + } + + onDisconnect() { + return () => undefined + } + + async close() { + this.state = 'closed' + } + + async emit(method: string, params: Record) { + await Promise.all([...this.listeners].map((listener) => listener({ method, params }))) + } +} + +const directories: string[] = [] + +async function makeDirectory() { + const directory = await mkdtemp(join(tmpdir(), 'nnd-har-')) + directories.push(directory) + return directory +} + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true }))) +}) + +describe('HAR 1.2 export', () => { + test('matches text, binary, failed, and traced requests from one session', async () => { + const directory = await makeDirectory() + const tap = new HarTap() + const binary = Buffer.from([0, 1, 254, 255]) + tap.bodies.set('text', { body: 'hello', base64Encoded: false }) + tap.bodies.set('binary', { body: binary.toString('base64'), base64Encoded: true }) + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + + await tap.emit('Network.requestWillBeSent', { + requestId: 'text', + timestamp: 1, + wallTime: 1_800_000_000, + type: 'Fetch', + request: { + url: 'https://example.test/text?tag=one&tag=two', + method: 'POST', + headers: { + 'content-type': 'text/plain', + 'x-request': 'captured', + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' + }, + postData: 'posted' + } + }) + await tap.emit('Network.responseReceived', { + requestId: 'text', + timestamp: 1.2, + type: 'Fetch', + response: { + url: 'https://example.test/text?tag=one&tag=two', + status: 201, + statusText: 'Created', + protocol: 'h2', + headers: { 'content-type': 'text/plain', 'x-response': 'captured' }, + mimeType: 'text/plain', + timing: { + dnsStart: 0, + dnsEnd: 2, + connectStart: 2, + connectEnd: 5, + sslStart: 3, + sslEnd: 5, + sendStart: 5, + sendEnd: 6 + } + } + }) + await tap.emit('Network.loadingFinished', { + requestId: 'text', + timestamp: 1.5, + encodedDataLength: 5 + }) + + await tap.emit('Network.requestWillBeSent', { + requestId: 'binary', + timestamp: 2, + wallTime: 1_800_000_001, + request: { + url: 'https://example.test/file.bin', + method: 'GET', + headers: {} + } + }) + await tap.emit('Network.responseReceived', { + requestId: 'binary', + timestamp: 2.1, + response: { + url: 'https://example.test/file.bin', + status: 206, + statusText: 'Partial Content', + protocol: 'http/1.1', + headers: { 'content-type': 'application/octet-stream' }, + mimeType: 'application/octet-stream' + } + }) + await tap.emit('Network.loadingFinished', { + requestId: 'binary', + timestamp: 2.2, + encodedDataLength: binary.length + }) + + await tap.emit('Network.requestWillBeSent', { + requestId: 'failed', + timestamp: 3, + wallTime: 1_800_000_002_000, + request: { + url: 'https://example.test/unavailable', + method: 'GET', + headers: { accept: '*/*' } + } + }) + await tap.emit('Network.loadingFailed', { + requestId: 'failed', + timestamp: 3.05, + errorText: 'net::ERR_CONNECTION_REFUSED', + canceled: false + }) + await recorder.close() + + const outputPath = join(directory, 'export.har') + const result = await exportHar(directory, outputPath) + expect(result.outputPath).toBe(outputPath) + expect(JSON.parse(await readFile(outputPath, 'utf8'))).toEqual(result.har) + expect(result.har.log).toMatchObject({ + version: '1.2', + creator: { name: 'node-network-devtools' } + }) + expect(result.har.log.entries).toHaveLength(3) + + const text = result.har.log.entries[0] + expect(text.request).toMatchObject({ + method: 'POST', + url: 'https://example.test/text?tag=one&tag=two', + httpVersion: 'HTTP/2', + queryString: [ + { name: 'tag', value: 'one' }, + { name: 'tag', value: 'two' } + ], + postData: { mimeType: 'text/plain', text: 'posted' } + }) + expect(text.request.headers).toContainEqual({ name: 'x-request', value: 'captured' }) + expect(text.response).toMatchObject({ + status: 201, + statusText: 'Created', + content: { size: 5, mimeType: 'text/plain', text: 'hello' } + }) + expect(text.response.content.encoding).toBeUndefined() + expect(text.time).toBeCloseTo(500) + expect(text.timings).toMatchObject({ dns: 2, connect: 3, send: 1, ssl: 2 }) + expect(text.timings.wait).toBeCloseTo(200) + expect(text.timings.receive).toBeCloseTo(300) + expect(text._trace).toMatchObject({ + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + parentId: '00f067aa0ba902b7' + }) + + const binaryEntry = result.har.log.entries[1] + expect(binaryEntry.response.content).toEqual({ + size: binary.length, + mimeType: 'application/octet-stream', + text: binary.toString('base64'), + encoding: 'base64' + }) + + const failure = result.har.log.entries[2] + expect(failure.response.status).toBe(0) + expect(failure.response.statusText).toBe('net::ERR_CONNECTION_REFUSED') + expect(failure._failure).toEqual({ + errorText: 'net::ERR_CONNECTION_REFUSED', + canceled: false + }) + expect(failure.startedDateTime).toBe(new Date(1_800_000_002_000).toISOString()) + }) + + test('rejects a body whose external bytes no longer match the manifest index', async () => { + const directory = await makeDirectory() + const tap = new HarTap() + tap.bodies.set('body', { body: 'original', base64Encoded: false }) + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + await tap.emit('Network.requestWillBeSent', { + requestId: 'body', + timestamp: 1, + request: { url: 'https://example.test', method: 'GET', headers: {} } + }) + await tap.emit('Network.responseReceived', { + requestId: 'body', + timestamp: 2, + response: { status: 200, headers: {}, mimeType: 'text/plain' } + }) + await tap.emit('Network.loadingFinished', { requestId: 'body', timestamp: 3 }) + await recorder.close() + const manifest = await readSessionManifest(directory) + await writeFile(join(directory, manifest.bodyIndex.body.path), 'tampered') + + await expect(buildHar(directory)).rejects.toThrow( + 'Session body integrity check failed for request body.' + ) + }) +}) diff --git a/packages/network-debugger/src/session/har.ts b/packages/network-debugger/src/session/har.ts new file mode 100644 index 0000000..94d59f5 --- /dev/null +++ b/packages/network-debugger/src/session/har.ts @@ -0,0 +1,207 @@ +import { promises as fs } from 'node:fs' +import { resolve } from 'node:path' +import { atomicWriteJson, readSessionManifest, resolveInside, sha256 } from './files' +import { sessionHeaderValue } from './trace' +import type { + HarDocument, + HarEntry, + HarExportResult, + HarHeader, + HarQueryParameter, + SessionBodyIndexEntry, + SessionManifest, + SessionRequestIndexEntry +} from './types' + +function asRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} +} + +function stringValue(value: unknown, fallback = ''): string { + return typeof value === 'string' ? value : value === undefined ? fallback : String(value) +} + +function finiteNumber(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined +} + +function headersToHar(headers: unknown): HarHeader[] { + const record = asRecord(headers) + const output: HarHeader[] = [] + for (const [name, value] of Object.entries(record)) { + if (Array.isArray(value)) { + for (const nested of value) output.push({ name, value: String(nested) }) + } else if (value !== undefined && value !== null) { + output.push({ name, value: String(value) }) + } + } + return output +} + +function queryString(url: string): HarQueryParameter[] { + try { + return [...new URL(url).searchParams].map(([name, value]) => ({ name, value })) + } catch { + return [] + } +} + +function millisecondsBetween(end: number | undefined, start: number | undefined): number { + if (end === undefined || start === undefined) return 0 + return Math.max(0, (end - start) * 1000) +} + +function timingDuration(timing: Record, start: string, end: string): number { + const startValue = finiteNumber(timing[start]) + const endValue = finiteNumber(timing[end]) + if (startValue === undefined || endValue === undefined || startValue < 0 || endValue < 0) { + return -1 + } + return Math.max(0, endValue - startValue) +} + +function startedDateTime(request: SessionRequestIndexEntry, fallback: string): string { + if (request.wallTime === undefined) return fallback + const milliseconds = + request.wallTime > 100_000_000_000 ? request.wallTime : request.wallTime * 1000 + const date = new Date(milliseconds) + return Number.isNaN(date.getTime()) ? fallback : date.toISOString() +} + +async function readIndexedBody( + sessionDirectory: string, + body: SessionBodyIndexEntry | undefined +): Promise { + if (!body) return undefined + const bytes = await fs.readFile(resolveInside(sessionDirectory, body.path)) + if (bytes.length !== body.byteLength || sha256(bytes) !== body.sha256) { + throw new Error(`Session body integrity check failed for request ${body.requestId}.`) + } + return bytes +} + +function responseProtocol(response: Record): string { + const protocol = stringValue(response.protocol) + if (!protocol) return 'HTTP/1.1' + if (protocol === 'h2') return 'HTTP/2' + if (/^http\//i.test(protocol)) return protocol.toUpperCase() + return protocol +} + +async function toHarEntry( + sessionDirectory: string, + manifest: SessionManifest, + indexed: SessionRequestIndexEntry +): Promise { + const request = asRecord(indexed.request) + const response = asRecord(indexed.response) + const requestHeaders = request.headers + const responseHeaders = response.headers + const url = stringValue(request.url, stringValue(response.url)) + const method = stringValue(request.method, 'GET') + const postData = typeof request.postData === 'string' ? request.postData : undefined + const bodyIndex = manifest.bodyIndex[indexed.requestId] + const body = await readIndexedBody(sessionDirectory, bodyIndex) + const responseMimeType = stringValue( + response.mimeType, + sessionHeaderValue(responseHeaders, 'content-type') ?? + bodyIndex?.mimeType ?? + 'application/octet-stream' + ) + const responseTimestamp = indexed.responseTimestamp + const terminalTimestamp = indexed.finishedTimestamp ?? responseTimestamp + const totalTime = millisecondsBetween(terminalTimestamp, indexed.requestTimestamp) + const responseTiming = asRecord(response.timing) + const wait = millisecondsBetween(responseTimestamp, indexed.requestTimestamp) + const receive = millisecondsBetween(terminalTimestamp, responseTimestamp) + const status = finiteNumber(response.status) ?? 0 + const requestBodySize = postData === undefined ? 0 : Buffer.byteLength(postData) + const responseBodySize = body?.length ?? indexed.encodedDataLength ?? 0 + + const entry: HarEntry = { + startedDateTime: startedDateTime(indexed, manifest.createdAt), + time: totalTime, + request: { + method, + url, + httpVersion: responseProtocol(response), + cookies: [], + headers: headersToHar(requestHeaders), + queryString: queryString(url), + ...(postData !== undefined + ? { + postData: { + mimeType: + sessionHeaderValue(requestHeaders, 'content-type') ?? 'application/octet-stream', + text: postData + } + } + : {}), + headersSize: -1, + bodySize: requestBodySize + }, + response: { + status, + statusText: stringValue(response.statusText, indexed.failure?.errorText ?? ''), + httpVersion: responseProtocol(response), + cookies: [], + headers: headersToHar(responseHeaders), + content: { + size: body?.length ?? indexed.encodedDataLength ?? 0, + mimeType: responseMimeType, + ...(body + ? bodyIndex?.base64Encoded + ? { text: body.toString('base64'), encoding: 'base64' as const } + : { text: body.toString('utf8') } + : {}) + }, + redirectURL: sessionHeaderValue(responseHeaders, 'location') ?? '', + headersSize: -1, + bodySize: responseBodySize + }, + cache: {}, + timings: { + blocked: timingDuration(responseTiming, 'proxyStart', 'proxyEnd'), + dns: timingDuration(responseTiming, 'dnsStart', 'dnsEnd'), + connect: timingDuration(responseTiming, 'connectStart', 'connectEnd'), + send: timingDuration(responseTiming, 'sendStart', 'sendEnd'), + wait, + receive, + ssl: timingDuration(responseTiming, 'sslStart', 'sslEnd') + }, + _requestId: indexed.requestId, + ...(indexed.failure ? { _failure: { ...indexed.failure } } : {}), + ...(indexed.trace ? { _trace: { ...indexed.trace } } : {}) + } + return entry +} + +export async function buildHar(sessionDirectory: string): Promise { + const directory = resolve(sessionDirectory) + const manifest = await readSessionManifest(directory) + const requests = Object.values(manifest.requestIndex).sort( + (left, right) => left.firstSequence - right.firstSequence + ) + const entries: HarEntry[] = [] + for (const request of requests) entries.push(await toHarEntry(directory, manifest, request)) + return { + log: { + version: '1.2', + creator: { name: 'node-network-devtools', version: '2' }, + pages: [], + entries + } + } +} + +export async function exportHar( + sessionDirectory: string, + outputPath = resolve(sessionDirectory, 'session.har') +): Promise { + const absoluteOutputPath = resolve(outputPath) + const har = await buildHar(sessionDirectory) + await atomicWriteJson(absoluteOutputPath, har) + return { outputPath: absoluteOutputPath, har } +} diff --git a/packages/network-debugger/src/session/index.ts b/packages/network-debugger/src/session/index.ts new file mode 100644 index 0000000..94eb334 --- /dev/null +++ b/packages/network-debugger/src/session/index.ts @@ -0,0 +1,37 @@ +export { + CdpCommandError, + ProtocolTap, + ProtocolTapError, + type ProtocolTapErrorCode +} from './protocol-tap' +export { SessionRecorder } from './recorder' +export { buildHar, exportHar } from './har' +export { readSessionManifest } from './files' +export { parseTraceparent, traceContextFromHeaders } from './trace' +export { + SESSION_SCHEMA_VERSION, + type CdpCommandId, + type CdpErrorObject, + type CdpProtocolEvent, + type HarContent, + type HarDocument, + type HarEntry, + type HarExportResult, + type HarHeader, + type HarPostData, + type HarQueryParameter, + type ProtocolTapCommandOptions, + type ProtocolTapOptions, + type ResponseBodyResult, + type SessionBodyIndexEntry, + type SessionEventRecord, + type SessionFailure, + type SessionManifest, + type SessionProtocolConnection, + type SessionRecorderIssue, + type SessionRecorderOptions, + type SessionRequestIndexEntry, + type SessionTraceIndexEntry, + type SessionTraceSpan, + type TraceContext +} from './types' diff --git a/packages/network-debugger/src/session/protocol-tap.test.ts b/packages/network-debugger/src/session/protocol-tap.test.ts new file mode 100644 index 0000000..5a1236e --- /dev/null +++ b/packages/network-debugger/src/session/protocol-tap.test.ts @@ -0,0 +1,217 @@ +import { createServer, get, type Server } from 'node:http' +import type { AddressInfo } from 'node:net' +import { WebSocketServer, type WebSocket } from 'ws' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import { ProtocolTap } from './protocol-tap' + +interface Harness { + server: Server + webSocketServer: WebSocketServer + target: DevtoolsTarget + commands: Array<{ id: number | string; method: string; params: Record }> + clients: Set +} + +async function listen(server: Server): Promise { + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + return (server.address() as AddressInfo).port +} + +async function createHarness(): Promise { + let target!: DevtoolsTarget + const commands: Harness['commands'] = [] + const clients = new Set() + const webSocketServer = new WebSocketServer({ noServer: true }) + const server = createServer((request, response) => { + if (request.url !== '/json/list') { + response.writeHead(404).end() + return + } + response.writeHead(200, { 'content-type': 'application/json' }) + response.end(JSON.stringify([target])) + }) + server.on('upgrade', (request, socket, head) => { + if (request.url !== '/devtools/page/session-integration') { + socket.destroy() + return + } + webSocketServer.handleUpgrade(request, socket, head, (client) => { + webSocketServer.emit('connection', client, request) + }) + }) + webSocketServer.on('connection', (client) => { + clients.add(client) + client.once('close', () => clients.delete(client)) + client.on('message', (raw) => { + const message = JSON.parse(raw.toString()) as { + id: number | string + method: string + params?: Record + } + commands.push({ id: message.id, method: message.method, params: message.params ?? {} }) + if (message.method === 'Network.enable') { + client.send(JSON.stringify({ id: message.id, result: {} })) + client.send( + JSON.stringify({ + method: 'Network.requestWillBeSent', + params: { requestId: 'from-real-websocket' } + }) + ) + } else if (message.method === 'Echo') { + const delay = Number(message.params?.delay ?? 0) + setTimeout(() => { + client.send(JSON.stringify({ id: message.id, result: { value: message.params?.value } })) + }, delay) + } else if (message.method === 'Fail') { + client.send( + JSON.stringify({ + id: message.id, + error: { code: -32000, message: 'intentional failure' } + }) + ) + } else if (message.method === 'Drop') { + client.close(1011, 'backend exited') + } else if (message.method === 'Malformed') { + client.send('{') + } + }) + }) + const port = await listen(server) + target = { + id: 'session-integration', + title: 'Session integration', + type: 'node', + url: '', + webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/session-integration`, + discoveryUrl: `http://127.0.0.1:${port}/json/list` + } + return { server, webSocketServer, target, commands, clients } +} + +async function discover(url: string): Promise { + return new Promise((resolve, reject) => { + get(url, (response) => { + const chunks: Buffer[] = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.on('end', () => { + try { + resolve((JSON.parse(Buffer.concat(chunks).toString()) as DevtoolsTarget[])[0]) + } catch (error) { + reject(error) + } + }) + }).on('error', reject) + }) +} + +async function closeHarness(harness: Harness): Promise { + for (const client of harness.clients) client.terminate() + await new Promise((resolve) => harness.webSocketServer.close(() => resolve())) + await new Promise((resolve) => harness.server.close(() => resolve())) +} + +describe('ProtocolTap real WebSocket transport', () => { + let harness: Harness + + beforeEach(async () => { + harness = await createHarness() + }) + + afterEach(async () => { + await closeHarness(harness) + }) + + test('connects through discovery, enables Network, dispatches events, and correlates commands', async () => { + const discovered = await discover(harness.target.discoveryUrl) + expect(discovered.webSocketDebuggerUrl).toBe(harness.target.webSocketDebuggerUrl) + const tap = new ProtocolTap(discovered) + const events: string[] = [] + tap.onEvent((event) => { + events.push(event.method) + }) + + await tap.connect() + const slow = tap.command<{ value: string }>('Echo', { value: 'slow', delay: 25 }) + const fast = tap.command<{ value: string }>('Echo', { value: 'fast', delay: 0 }) + + await expect(fast).resolves.toEqual({ value: 'fast' }) + await expect(slow).resolves.toEqual({ value: 'slow' }) + await expect(tap.command('Fail')).rejects.toMatchObject({ + name: 'CdpCommandError', + method: 'Fail', + cdpError: { code: -32000, message: 'intentional failure' } + }) + expect(events).toContain('Network.requestWillBeSent') + expect(harness.commands.filter(({ method }) => method === 'Network.enable')).toHaveLength(1) + + const firstClose = tap.close() + expect(tap.close()).toBe(firstClose) + await firstClose + expect(tap.state).toBe('closed') + }) + + test('bounds pending commands and rejects the waiter when closed', async () => { + const tap = new ProtocolTap(harness.target, { + maxPendingCommands: 1, + commandTimeoutMs: 30_000 + }) + await tap.connect() + const held = tap.command('Hold') + const heldRejection = expect(held).rejects.toMatchObject({ + code: 'SESSION_TAP_CLOSED' + }) + + await expect(tap.command('Second')).rejects.toMatchObject({ + code: 'SESSION_TAP_PENDING_LIMIT' + }) + expect(tap.pendingCommandCount).toBe(1) + await tap.close() + await heldRejection + expect(tap.pendingCommandCount).toBe(0) + }) + + test('expires unanswered commands without retaining pending state', async () => { + const tap = new ProtocolTap(harness.target, { commandTimeoutMs: 20 }) + await tap.connect() + + await expect(tap.command('Hold')).rejects.toMatchObject({ + code: 'SESSION_TAP_COMMAND_TIMEOUT' + }) + expect(tap.pendingCommandCount).toBe(0) + await tap.close() + }) + + test('surfaces abnormal disconnects to commands and observers', async () => { + const tap = new ProtocolTap(harness.target) + const disconnects: Error[] = [] + tap.onDisconnect((error) => disconnects.push(error)) + await tap.connect() + + await expect(tap.command('Drop')).rejects.toMatchObject({ + code: 'SESSION_TAP_DISCONNECTED' + }) + expect(disconnects).toHaveLength(1) + await expect(tap.command('AfterDrop')).rejects.toMatchObject({ + code: 'SESSION_TAP_DISCONNECTED' + }) + await tap.close() + }) + + test('treats malformed protocol JSON as a terminal protocol error', async () => { + const tap = new ProtocolTap(harness.target) + await tap.connect() + + await expect(tap.command('Malformed')).rejects.toMatchObject({ + code: 'SESSION_TAP_PROTOCOL_ERROR' + }) + expect(tap.pendingCommandCount).toBe(0) + await tap.close() + }) +}) diff --git a/packages/network-debugger/src/session/protocol-tap.ts b/packages/network-debugger/src/session/protocol-tap.ts new file mode 100644 index 0000000..3591744 --- /dev/null +++ b/packages/network-debugger/src/session/protocol-tap.ts @@ -0,0 +1,479 @@ +import WebSocket, { type RawData } from 'ws' +import type { DevtoolsTarget } from '../adapters/types' +import { withoutLegacyCapture } from '../core/capture-scope' +import type { + CdpCommandId, + CdpErrorObject, + CdpProtocolEvent, + ProtocolTapCommandOptions, + ProtocolTapOptions, + SessionProtocolConnection +} from './types' + +const DEFAULT_CONNECT_TIMEOUT_MS = 10_000 +const DEFAULT_COMMAND_TIMEOUT_MS = 10_000 +const DEFAULT_CLOSE_TIMEOUT_MS = 2_000 +const DEFAULT_MAX_PENDING_COMMANDS = 256 + +export type ProtocolTapErrorCode = + | 'SESSION_TAP_CLOSED' + | 'SESSION_TAP_DISCONNECTED' + | 'SESSION_TAP_CONNECT_TIMEOUT' + | 'SESSION_TAP_COMMAND_TIMEOUT' + | 'SESSION_TAP_PENDING_LIMIT' + | 'SESSION_TAP_PROTOCOL_ERROR' + +export class ProtocolTapError extends Error { + readonly cause?: unknown + + constructor( + readonly code: ProtocolTapErrorCode, + message: string, + cause?: unknown + ) { + super(message) + this.name = 'ProtocolTapError' + this.cause = cause + } +} + +export class CdpCommandError extends Error { + constructor( + readonly method: string, + readonly commandId: CdpCommandId, + readonly cdpError: CdpErrorObject + ) { + super(`CDP command ${method} failed (${cdpError.code}): ${cdpError.message}`) + this.name = 'CdpCommandError' + } +} + +interface PendingCommand { + method: string + timer: ReturnType + resolve(value: unknown): void + reject(error: Error): void +} + +interface CdpResponse { + id: CdpCommandId + result?: unknown + error?: CdpErrorObject +} + +function positiveInteger(value: number | undefined, fallback: number): number { + return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +/** A small, backend-neutral CDP client used by recording and export features. */ +export class ProtocolTap implements SessionProtocolConnection { + readonly target: DevtoolsTarget + + private readonly connectTimeoutMs: number + private readonly commandTimeoutMs: number + private readonly closeTimeoutMs: number + private readonly maxPendingCommands: number + private readonly eventListeners = new Set<(event: CdpProtocolEvent) => void | Promise>() + private readonly methodListeners = new Map< + string, + Set<(params: Record, event: CdpProtocolEvent) => void | Promise> + >() + private readonly disconnectListeners = new Set<(error: Error) => void>() + private readonly errorListeners = new Set<(error: Error) => void>() + private readonly pending = new Map() + + private socket?: WebSocket + private connectPromise?: Promise + private closePromise?: Promise + private nextCommandId = 0 + private _state: SessionProtocolConnection['state'] = 'idle' + private terminalError?: Error + + constructor(target: DevtoolsTarget, options: ProtocolTapOptions = {}) { + this.target = { ...target } + this.connectTimeoutMs = positiveInteger(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS) + this.commandTimeoutMs = positiveInteger(options.commandTimeoutMs, DEFAULT_COMMAND_TIMEOUT_MS) + this.closeTimeoutMs = positiveInteger(options.closeTimeoutMs, DEFAULT_CLOSE_TIMEOUT_MS) + this.maxPendingCommands = positiveInteger( + options.maxPendingCommands, + DEFAULT_MAX_PENDING_COMMANDS + ) + } + + get state(): SessionProtocolConnection['state'] { + return this._state + } + + get pendingCommandCount(): number { + return this.pending.size + } + + connect(): Promise { + if (this._state === 'open') return Promise.resolve(this) + if (this._state === 'connecting' && this.connectPromise) return this.connectPromise + if (this._state === 'closing' || this._state === 'closed') { + return Promise.reject( + new ProtocolTapError('SESSION_TAP_CLOSED', 'Cannot connect a closed ProtocolTap.') + ) + } + + this._state = 'connecting' + this.connectPromise = this.open() + return this.connectPromise + } + + private async open(): Promise { + let socket: WebSocket + try { + // In Legacy mode the application HTTP APIs are patched. The recorder's + // own CDP transport must not appear as business traffic or recursively + // observe the events it causes itself. + socket = withoutLegacyCapture(() => new WebSocket(this.target.webSocketDebuggerUrl)) + } catch (error) { + const wrapped = new ProtocolTapError( + 'SESSION_TAP_DISCONNECTED', + `Could not create CDP WebSocket ${this.target.webSocketDebuggerUrl}: ${errorMessage(error)}`, + error + ) + this.handleDisconnect(wrapped) + throw wrapped + } + this.socket = socket + socket.on('message', (data) => this.handleMessage(data)) + socket.on('error', (error) => { + this.handleDisconnect( + new ProtocolTapError( + 'SESSION_TAP_DISCONNECTED', + `CDP WebSocket error: ${error.message}`, + error + ) + ) + }) + socket.on('close', (code, reason) => { + this.handleDisconnect( + new ProtocolTapError( + 'SESSION_TAP_DISCONNECTED', + `CDP WebSocket closed (${code}): ${reason.toString() || 'no reason'}` + ) + ) + }) + + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + reject( + new ProtocolTapError( + 'SESSION_TAP_CONNECT_TIMEOUT', + `Timed out after ${this.connectTimeoutMs}ms connecting to ${this.target.webSocketDebuggerUrl}.` + ) + ) + }, this.connectTimeoutMs) + const cleanup = () => { + clearTimeout(timer) + socket.off('open', onOpen) + socket.off('close', onClose) + socket.off('error', onError) + socket.off('unexpected-response', onUnexpectedResponse) + } + const onOpen = () => { + cleanup() + resolve() + } + const onClose = () => { + cleanup() + reject( + this.terminalError ?? + new ProtocolTapError( + 'SESSION_TAP_DISCONNECTED', + 'CDP WebSocket closed before opening.' + ) + ) + } + const onError = (error: Error) => { + cleanup() + reject(this.terminalError ?? error) + } + const onUnexpectedResponse = ( + _request: import('node:http').ClientRequest, + response: import('node:http').IncomingMessage + ) => { + cleanup() + reject( + new ProtocolTapError( + 'SESSION_TAP_DISCONNECTED', + `Unexpected HTTP ${response.statusCode ?? 0} while connecting to ${this.target.webSocketDebuggerUrl}.` + ) + ) + } + socket.once('open', onOpen) + socket.once('close', onClose) + socket.once('error', onError) + socket.once('unexpected-response', onUnexpectedResponse) + }) + + if (this._state === 'closed' || socket.readyState !== WebSocket.OPEN) { + throw ( + this.terminalError ?? + new ProtocolTapError('SESSION_TAP_DISCONNECTED', 'CDP WebSocket did not remain open.') + ) + } + this._state = 'open' + await this.command('Network.enable') + return this + } catch (error) { + if (this._state !== 'closed') { + const wrapped = + error instanceof Error + ? error + : new ProtocolTapError('SESSION_TAP_DISCONNECTED', errorMessage(error), error) + this.handleDisconnect(wrapped) + } + if (socket.readyState !== WebSocket.CLOSED) socket.terminate() + throw this.terminalError ?? error + } + } + + command>( + method: string, + params: Record = {}, + options: ProtocolTapCommandOptions = {} + ): Promise { + const socket = this.socket + if (this._state !== 'open' || !socket || socket.readyState !== WebSocket.OPEN) { + return Promise.reject( + this.terminalError ?? + new ProtocolTapError( + 'SESSION_TAP_CLOSED', + `Cannot send ${method}: CDP WebSocket is not open.` + ) + ) + } + if (this.pending.size >= this.maxPendingCommands) { + return Promise.reject( + new ProtocolTapError( + 'SESSION_TAP_PENDING_LIMIT', + `Cannot send ${method}: pending command limit ${this.maxPendingCommands} reached.` + ) + ) + } + + const id = this.allocateCommandId() + const timeoutMs = positiveInteger(options.timeoutMs, this.commandTimeoutMs) + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id) + reject( + new ProtocolTapError( + 'SESSION_TAP_COMMAND_TIMEOUT', + `Timed out after ${timeoutMs}ms waiting for ${method} (${id}).` + ) + ) + }, timeoutMs) + this.pending.set(id, { + method, + timer, + resolve: (value) => resolve(value as T), + reject + }) + + socket.send(JSON.stringify({ id, method, params }), (error) => { + if (!error) return + const pending = this.pending.get(id) + if (!pending) return + this.pending.delete(id) + clearTimeout(pending.timer) + pending.reject(error) + }) + }) + } + + onEvent(listener: (event: CdpProtocolEvent) => void | Promise): () => void { + this.eventListeners.add(listener) + return () => this.eventListeners.delete(listener) + } + + on( + method: string, + listener: (params: Record, event: CdpProtocolEvent) => void | Promise + ): () => void { + const listeners = this.methodListeners.get(method) ?? new Set() + listeners.add(listener) + this.methodListeners.set(method, listeners) + return () => { + listeners.delete(listener) + if (listeners.size === 0) this.methodListeners.delete(method) + } + } + + onDisconnect(listener: (error: Error) => void): () => void { + this.disconnectListeners.add(listener) + return () => this.disconnectListeners.delete(listener) + } + + onError(listener: (error: Error) => void): () => void { + this.errorListeners.add(listener) + return () => this.errorListeners.delete(listener) + } + + close(): Promise { + if (this.closePromise) return this.closePromise + this.closePromise = this.closeSocket() + return this.closePromise + } + + private async closeSocket(): Promise { + if (this._state === 'closed') return + if (this._state === 'idle') { + this._state = 'closed' + return + } + + this._state = 'closing' + this.rejectPending( + new ProtocolTapError('SESSION_TAP_CLOSED', 'ProtocolTap closed before command completion.') + ) + const socket = this.socket + if (!socket || socket.readyState === WebSocket.CLOSED) { + this._state = 'closed' + return + } + + await new Promise((resolve) => { + let finished = false + const finish = () => { + if (finished) return + finished = true + clearTimeout(timer) + socket.off('close', finish) + resolve() + } + const timer = setTimeout(() => { + socket.terminate() + finish() + }, this.closeTimeoutMs) + socket.once('close', finish) + if (socket.readyState === WebSocket.CONNECTING) socket.terminate() + else socket.close() + }) + this._state = 'closed' + } + + private allocateCommandId(): number { + do { + this.nextCommandId += 1 + if (!Number.isSafeInteger(this.nextCommandId)) this.nextCommandId = 1 + } while (this.pending.has(this.nextCommandId)) + return this.nextCommandId + } + + private handleMessage(data: RawData): void { + let message: unknown + try { + message = JSON.parse(data.toString()) + } catch (error) { + const wrapped = new ProtocolTapError( + 'SESSION_TAP_PROTOCOL_ERROR', + `Received malformed CDP JSON: ${errorMessage(error)}`, + error + ) + this.emitError(wrapped) + this.handleDisconnect(wrapped) + return + } + if (!message || typeof message !== 'object' || Array.isArray(message)) return + const value = message as Record + + if (typeof value.id === 'number' || typeof value.id === 'string') { + this.handleResponse(value as unknown as CdpResponse) + return + } + if (typeof value.method !== 'string') return + const event: CdpProtocolEvent = { + method: value.method, + params: + value.params && typeof value.params === 'object' && !Array.isArray(value.params) + ? (value.params as Record) + : {} + } + this.emitEvent(event) + } + + private handleResponse(response: CdpResponse): void { + const pending = this.pending.get(response.id) + if (!pending) return + this.pending.delete(response.id) + clearTimeout(pending.timer) + if (response.error) { + pending.reject(new CdpCommandError(pending.method, response.id, response.error)) + } else { + pending.resolve(response.result) + } + } + + private emitEvent(event: CdpProtocolEvent): void { + for (const listener of [...this.eventListeners]) this.invokeListener(listener, event) + for (const listener of [...(this.methodListeners.get(event.method) ?? [])]) { + this.invokeListener(listener, event.params, event) + } + } + + private invokeListener(listener: (...args: any[]) => unknown, ...args: unknown[]): void { + try { + const result = listener(...args) + if (result && typeof (result as PromiseLike).then === 'function') { + void Promise.resolve(result).catch((error) => this.emitError(asError(error))) + } + } catch (error) { + this.emitError(asError(error)) + } + } + + private emitError(error: Error): void { + for (const listener of [...this.errorListeners]) { + try { + listener(error) + } catch { + // Error observers must not destabilize the protocol transport. + } + } + } + + private handleDisconnect(error: Error): void { + if (this._state === 'closed') return + const intentional = this._state === 'closing' + this.terminalError = intentional + ? new ProtocolTapError('SESSION_TAP_CLOSED', 'ProtocolTap closed.') + : error + this._state = 'closed' + this.rejectPending(this.terminalError) + if (!intentional && this.socket && this.socket.readyState !== WebSocket.CLOSED) { + this.socket.terminate() + } + if (!intentional) { + for (const listener of [...this.disconnectListeners]) { + try { + listener(error) + } catch (listenerError) { + this.emitError(asError(listenerError)) + } + } + } + } + + private rejectPending(error: Error): void { + for (const pending of this.pending.values()) { + clearTimeout(pending.timer) + pending.reject(error) + } + this.pending.clear() + } +} + +function asError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)) +} diff --git a/packages/network-debugger/src/session/recorder.integration.test.ts b/packages/network-debugger/src/session/recorder.integration.test.ts new file mode 100644 index 0000000..46872fc --- /dev/null +++ b/packages/network-debugger/src/session/recorder.integration.test.ts @@ -0,0 +1,137 @@ +import { createServer } from 'node:http' +import type { AddressInfo } from 'node:net' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { WebSocketServer, type WebSocket } from 'ws' +import { afterEach, describe, expect, test } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import { readSessionManifest } from './files' +import { SessionRecorder } from './recorder' + +const cleanups: Array<() => Promise> = [] + +afterEach(async () => { + for (const cleanup of cleanups.splice(0).reverse()) await cleanup() +}) + +describe('SessionRecorder loopback integration', () => { + test('records events and retrieves a body over a real standard target WebSocket', async () => { + const clients = new Set() + let target!: DevtoolsTarget + let resolveBodyRequested!: () => void + const bodyRequested = new Promise((resolve) => (resolveBodyRequested = resolve)) + const webSocketServer = new WebSocketServer({ noServer: true }) + const server = createServer((request, response) => { + if (request.url !== '/json/list') { + response.writeHead(404).end() + return + } + response.writeHead(200, { 'content-type': 'application/json' }) + response.end(JSON.stringify([target])) + }) + server.on('upgrade', (request, socket, head) => { + if (request.url !== '/devtools/page/session-recorder') { + socket.destroy() + return + } + webSocketServer.handleUpgrade(request, socket, head, (client) => { + webSocketServer.emit('connection', client, request) + }) + }) + webSocketServer.on('connection', (client) => { + clients.add(client) + client.once('close', () => clients.delete(client)) + client.on('message', (raw) => { + const command = JSON.parse(raw.toString()) as { + id: number + method: string + params: Record + } + if (command.method === 'Network.enable') { + client.send(JSON.stringify({ id: command.id, result: {} })) + } else if (command.method === 'Network.getResponseBody') { + expect(command.params).toEqual({ requestId: 'integration' }) + resolveBodyRequested() + client.send( + JSON.stringify({ + id: command.id, + result: { body: 'loopback body', base64Encoded: false } + }) + ) + } + }) + }) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => resolve()) + }) + const port = (server.address() as AddressInfo).port + target = { + id: 'session-recorder', + title: 'Session recorder', + type: 'node', + url: '', + webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/session-recorder`, + discoveryUrl: `http://127.0.0.1:${port}/json/list` + } + const directory = await mkdtemp(join(tmpdir(), 'nnd-session-integration-')) + cleanups.push(async () => rm(directory, { recursive: true })) + cleanups.push(async () => { + for (const client of clients) client.terminate() + await new Promise((resolve) => webSocketServer.close(() => resolve())) + await new Promise((resolve) => server.close(() => resolve())) + }) + + const recorder = await SessionRecorder.start({ directory, target }) + const client = [...clients][0] + client.send( + JSON.stringify({ + method: 'Network.requestWillBeSent', + params: { + requestId: 'integration', + timestamp: 1, + wallTime: 1_800_000_000, + request: { + url: `http://127.0.0.1:${port}/resource`, + method: 'GET', + headers: {} + } + } + }) + ) + client.send( + JSON.stringify({ + method: 'Network.responseReceived', + params: { + requestId: 'integration', + timestamp: 1.1, + response: { + url: `http://127.0.0.1:${port}/resource`, + status: 200, + statusText: 'OK', + headers: { 'content-type': 'text/plain' }, + mimeType: 'text/plain' + } + } + }) + ) + client.send( + JSON.stringify({ + method: 'Network.loadingFinished', + params: { requestId: 'integration', timestamp: 1.2, encodedDataLength: 13 } + }) + ) + + await bodyRequested + await recorder.close() + + const manifest = await readSessionManifest(directory) + expect(manifest.state).toBe('completed') + expect(manifest.stats).toMatchObject({ eventCount: 3, requestCount: 1, bodyCount: 1 }) + expect(await readFile(join(directory, manifest.bodyIndex.integration.path), 'utf8')).toBe( + 'loopback body' + ) + expect(clients.size).toBe(0) + }) +}) diff --git a/packages/network-debugger/src/session/recorder.test.ts b/packages/network-debugger/src/session/recorder.test.ts new file mode 100644 index 0000000..f5b4ff5 --- /dev/null +++ b/packages/network-debugger/src/session/recorder.test.ts @@ -0,0 +1,281 @@ +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises' +import { basename, join } from 'node:path' +import { tmpdir } from 'node:os' +import { afterEach, describe, expect, test, vi } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' +import { readSessionManifest } from './files' +import { SessionRecorder } from './recorder' +import type { CdpProtocolEvent, ResponseBodyResult, SessionProtocolConnection } from './types' + +const TARGET: DevtoolsTarget = { + id: 'session-test', + title: 'Session test', + type: 'node', + url: 'file:///fixture.js', + webSocketDebuggerUrl: 'ws://127.0.0.1:43123/devtools/page/session-test', + discoveryUrl: 'http://127.0.0.1:43123/json/list' +} + +class FakeTap implements SessionProtocolConnection { + readonly target = TARGET + state: SessionProtocolConnection['state'] = 'idle' + readonly commands: Array<{ method: string; params: Record }> = [] + readonly bodies = new Map() + readonly closeMock = vi.fn(async () => { + this.state = 'closed' + }) + private readonly eventListeners = new Set<(event: CdpProtocolEvent) => void | Promise>() + private readonly disconnectListeners = new Set<(error: Error) => void>() + + async connect() { + this.state = 'open' + } + + async command(method: string, params: Record = {}): Promise { + this.commands.push({ method, params: { ...params } }) + const requestId = String(params.requestId ?? '') + const value = this.bodies.get(requestId) + if (value instanceof Error) throw value + if (!value) throw new Error(`No body for ${requestId}`) + return value as T + } + + onEvent(listener: (event: CdpProtocolEvent) => void | Promise) { + this.eventListeners.add(listener) + return () => this.eventListeners.delete(listener) + } + + onDisconnect(listener: (error: Error) => void) { + this.disconnectListeners.add(listener) + return () => this.disconnectListeners.delete(listener) + } + + close() { + return this.closeMock() + } + + async emit(method: string, params: Record) { + const event = { method, params } + await Promise.all([...this.eventListeners].map((listener) => listener(event))) + } + + disconnect(error: Error) { + this.state = 'closed' + for (const listener of this.disconnectListeners) listener(error) + } +} + +const directories: string[] = [] + +async function sessionDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), 'nnd-session-')) + directories.push(directory) + return directory +} + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true }))) +}) + +async function requestStarted( + tap: FakeTap, + requestId: string, + overrides: Record = {} +) { + await tap.emit('Network.requestWillBeSent', { + requestId, + timestamp: 10, + wallTime: 1_800_000_000, + type: 'Fetch', + request: { + url: `https://example.test/resource?id=${requestId}`, + method: 'POST', + headers: { 'content-type': 'text/plain' }, + postData: 'request body' + }, + ...overrides + }) +} + +async function responseReceived(tap: FakeTap, requestId: string, mimeType: string) { + await tap.emit('Network.responseReceived', { + requestId, + timestamp: 10.25, + type: 'Fetch', + response: { + url: `https://example.test/resource?id=${requestId}`, + status: 200, + statusText: 'OK', + protocol: 'h2', + headers: { 'content-type': mimeType }, + mimeType, + encodedDataLength: 3 + } + }) +} + +describe('SessionRecorder', () => { + test('writes text bodies, an append-only journal, and trace indexes', async () => { + const directory = await sessionDirectory() + const tap = new FakeTap() + tap.bodies.set('../unsafe/request', { body: 'hello session', base64Encoded: false }) + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + const headers = { + 'content-type': 'text/plain', + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + tracestate: 'vendor=value' + } + const before = JSON.stringify(headers) + + await requestStarted(tap, '../unsafe/request', { + request: { + url: 'https://example.test/text?one=1&one=2', + method: 'POST', + headers, + postData: 'request body' + } + }) + await responseReceived(tap, '../unsafe/request', 'text/plain') + await tap.emit('Network.loadingFinished', { + requestId: '../unsafe/request', + timestamp: 10.5, + encodedDataLength: 13 + }) + await recorder.close() + + expect(JSON.stringify(headers)).toBe(before) + const manifest = await readSessionManifest(directory) + expect(manifest).toMatchObject({ + schemaVersion: 1, + state: 'completed', + stats: { + eventCount: 3, + requestCount: 1, + bodyCount: 1, + bodyErrorCount: 0, + failedRequestCount: 0 + } + }) + expect(manifest.traceIndex['4bf92f3577b34da6a3ce929d0e0e4736']).toMatchObject({ + requestIds: ['../unsafe/request'], + spans: [{ requestId: '../unsafe/request', sampled: true }] + }) + const indexedBody = manifest.bodyIndex['../unsafe/request'] + expect(basename(indexedBody.path)).toMatch(/^[a-f0-9]{16}-[a-f0-9]{64}\.body$/) + expect(indexedBody.path).not.toContain('unsafe') + expect(await readFile(join(directory, indexedBody.path), 'utf8')).toBe('hello session') + expect( + (await readFile(join(directory, 'events.ndjson'), 'utf8')).trim().split('\n') + ).toHaveLength(3) + expect(tap.commands).toEqual([ + { method: 'Network.getResponseBody', params: { requestId: '../unsafe/request' } } + ]) + expect(tap.commands.some(({ method }) => method === 'Network.setExtraHTTPHeaders')).toBe(false) + }) + + test('decodes base64 response bodies to original binary files', async () => { + const directory = await sessionDirectory() + const tap = new FakeTap() + const binary = Buffer.from([0, 255, 16, 128]) + tap.bodies.set('binary', { body: binary.toString('base64'), base64Encoded: true }) + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + + await requestStarted(tap, 'binary') + await responseReceived(tap, 'binary', 'application/octet-stream') + await tap.emit('Network.loadingFinished', { + requestId: 'binary', + timestamp: 11, + encodedDataLength: binary.length + }) + await recorder.close() + + const manifest = await readSessionManifest(directory) + const indexed = manifest.bodyIndex.binary + expect(indexed).toMatchObject({ base64Encoded: true, byteLength: 4 }) + expect(await readFile(join(directory, indexed.path))).toEqual(binary) + }) + + test('records failures without requesting or inventing a response body', async () => { + const directory = await sessionDirectory() + const tap = new FakeTap() + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + + await requestStarted(tap, 'failed') + await tap.emit('Network.loadingFailed', { + requestId: 'failed', + timestamp: 10.1, + errorText: 'net::ERR_CONNECTION_REFUSED', + canceled: false + }) + await recorder.close() + + const manifest = await readSessionManifest(directory) + expect(manifest.stats).toMatchObject({ failedRequestCount: 1, bodyCount: 0 }) + expect(manifest.requestIndex.failed.failure).toEqual({ + errorText: 'net::ERR_CONNECTION_REFUSED', + canceled: false + }) + expect(tap.commands).toEqual([]) + }) + + test('keeps recording when getResponseBody is unavailable and indexes the issue', async () => { + const directory = await sessionDirectory() + const tap = new FakeTap() + tap.bodies.set('missing', new Error('No resource with given identifier found')) + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + + await requestStarted(tap, 'missing') + await responseReceived(tap, 'missing', 'text/plain') + await tap.emit('Network.loadingFinished', { + requestId: 'missing', + timestamp: 10.5, + encodedDataLength: 0 + }) + await recorder.close() + + const manifest = await readSessionManifest(directory) + expect(manifest.state).toBe('completed') + expect(manifest.stats).toMatchObject({ bodyCount: 0, bodyErrorCount: 1 }) + expect(manifest.issues).toContainEqual( + expect.objectContaining({ + operation: 'Network.getResponseBody', + requestId: 'missing', + message: 'No resource with given identifier found' + }) + ) + }) + + test('marks unexpected disconnects failed and closes idempotently without owning the tap', async () => { + const directory = await sessionDirectory() + const tap = new FakeTap() + const recorder = await SessionRecorder.start({ directory, target: TARGET, tap }) + tap.disconnect(new Error('backend exited')) + + const firstClose = recorder.close() + expect(recorder.close()).toBe(firstClose) + await firstClose + await tap.emit('Network.requestWillBeSent', { requestId: 'too-late' }) + + const manifest = await readSessionManifest(directory) + expect(manifest.state).toBe('failed') + expect(manifest.stats.eventCount).toBe(0) + expect(manifest.issues).toContainEqual( + expect.objectContaining({ operation: 'protocol-disconnect', message: 'backend exited' }) + ) + expect(tap.closeMock).not.toHaveBeenCalled() + expect((await readdir(directory)).some((name) => name.endsWith('.tmp'))).toBe(false) + }) + + test('refuses to overwrite an existing session directory', async () => { + const directory = await sessionDirectory() + const first = await SessionRecorder.start({ directory, target: TARGET, tap: new FakeTap() }) + await first.close() + const before = await readSessionManifest(directory) + + await expect( + SessionRecorder.start({ directory, target: TARGET, tap: new FakeTap() }) + ).rejects.toThrow(`Session artifacts already exist in: ${directory}`) + + expect((await readSessionManifest(directory)).sessionId).toBe(before.sessionId) + }) +}) diff --git a/packages/network-debugger/src/session/recorder.ts b/packages/network-debugger/src/session/recorder.ts new file mode 100644 index 0000000..fa5db03 --- /dev/null +++ b/packages/network-debugger/src/session/recorder.ts @@ -0,0 +1,379 @@ +import { randomUUID } from 'node:crypto' +import { promises as fs } from 'node:fs' +import type { FileHandle } from 'node:fs/promises' +import { resolve } from 'node:path' +import { ProtocolTap } from './protocol-tap' +import { atomicWriteJson, bodyFileName, jsonStringify, resolveInside, sha256 } from './files' +import { traceContextFromHeaders } from './trace' +import { + SESSION_SCHEMA_VERSION, + type CdpProtocolEvent, + type ResponseBodyResult, + type SessionBodyIndexEntry, + type SessionManifest, + type SessionProtocolConnection, + type SessionRecorderIssue, + type SessionRecorderOptions, + type SessionRequestIndexEntry, + type SessionTraceIndexEntry +} from './types' + +const DEFAULT_BODY_COMMAND_TIMEOUT_MS = 10_000 + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : undefined +} + +function cloneRecord(value: unknown): Record | undefined { + const record = asRecord(value) + return record ? (JSON.parse(jsonStringify(record)) as Record) : undefined +} + +function requestIdFrom(params: Record): string | undefined { + const requestId = params.requestId + if (typeof requestId === 'string') return requestId + if (typeof requestId === 'number' && Number.isFinite(requestId)) return String(requestId) + return undefined +} + +function finiteNumber(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined +} + +function messageFor(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +async function pathExists(path: string): Promise { + try { + await fs.access(path) + return true + } catch { + return false + } +} + +/** Records standard CDP Network events into a portable, append-only session. */ +export class SessionRecorder { + readonly directory: string + readonly target: SessionManifest['target'] + readonly tap: SessionProtocolConnection + + private readonly ownsTap: boolean + private readonly bodyCommandTimeoutMs: number + private readonly manifestPath: string + private readonly eventsPath: string + private readonly bodiesPath: string + private readonly requestIndex = new Map() + private readonly bodyIndex = new Map() + private readonly traceIndex = new Map() + private readonly issues: SessionRecorderIssue[] = [] + private readonly bodyErrors = new Set() + private readonly terminalRequests = new Set() + private readonly baseManifest: Omit< + SessionManifest, + 'stats' | 'requestIndex' | 'bodyIndex' | 'traceIndex' | 'issues' + > + + private eventFile?: FileHandle + private unsubscribeEvent?: () => void + private unsubscribeDisconnect?: () => void + private operationQueue: Promise = Promise.resolve() + private closePromise?: Promise + private fatalError?: Error + private acceptingEvents = true + private sequence = 0 + + private constructor(options: SessionRecorderOptions) { + this.directory = resolve(options.directory) + this.target = { ...options.target } + this.tap = options.tap ?? new ProtocolTap(this.target) + this.ownsTap = options.tap === undefined + this.bodyCommandTimeoutMs = + Number.isSafeInteger(options.bodyCommandTimeoutMs) && Number(options.bodyCommandTimeoutMs) > 0 + ? Number(options.bodyCommandTimeoutMs) + : DEFAULT_BODY_COMMAND_TIMEOUT_MS + this.manifestPath = resolve(this.directory, 'manifest.json') + this.eventsPath = resolve(this.directory, 'events.ndjson') + this.bodiesPath = resolve(this.directory, 'bodies') + this.baseManifest = { + schemaVersion: SESSION_SCHEMA_VERSION, + sessionId: randomUUID(), + state: 'recording', + createdAt: new Date().toISOString(), + target: this.target, + files: { events: 'events.ndjson', bodies: 'bodies' } + } + } + + static async start(options: SessionRecorderOptions): Promise { + if ( + options.tap && + options.tap.target.webSocketDebuggerUrl !== options.target.webSocketDebuggerUrl + ) { + throw new Error('Session recorder target does not match the supplied ProtocolTap target.') + } + const recorder = new SessionRecorder(options) + await recorder.initialize() + return recorder + } + + getManifest(): SessionManifest { + return JSON.parse(jsonStringify(this.buildManifest())) as SessionManifest + } + + close(): Promise { + if (!this.closePromise) this.closePromise = this.finish() + return this.closePromise + } + + private async initialize(): Promise { + let artifactsStarted = false + try { + await fs.mkdir(this.directory, { recursive: true }) + if ((await pathExists(this.manifestPath)) || (await pathExists(this.eventsPath))) { + throw new Error(`Session artifacts already exist in: ${this.directory}`) + } + await fs.mkdir(this.bodiesPath, { recursive: true }) + this.eventFile = await fs.open(this.eventsPath, 'wx') + artifactsStarted = true + await this.persistManifest() + + this.unsubscribeEvent = this.tap.onEvent((event) => { + if (!this.acceptingEvents) return + return this.enqueue(() => this.recordEvent(event)) + }) + this.unsubscribeDisconnect = this.tap.onDisconnect((error) => { + if (!this.acceptingEvents) return + void this.enqueue(async () => { + this.baseManifest.state = 'failed' + this.addIssue('protocol-disconnect', error) + await this.persistManifest() + }) + }) + await this.tap.connect() + } catch (error) { + this.acceptingEvents = false + this.unsubscribeEvent?.() + this.unsubscribeDisconnect?.() + await this.operationQueue + if (artifactsStarted) { + this.baseManifest.state = 'failed' + this.baseManifest.completedAt = new Date().toISOString() + this.addIssue('session-start', error) + await this.eventFile?.close().catch(() => undefined) + this.eventFile = undefined + await this.persistManifest().catch(() => undefined) + } + if (this.ownsTap) await this.tap.close().catch(() => undefined) + throw error + } + } + + private enqueue(operation: () => Promise): Promise { + const scheduled = this.operationQueue.then(operation) + this.operationQueue = scheduled.catch((error) => { + const normalized = error instanceof Error ? error : new Error(String(error)) + if (!this.fatalError) { + this.fatalError = normalized + this.baseManifest.state = 'failed' + this.addIssue('session-write', normalized) + } + }) + return scheduled + } + + private async recordEvent(event: CdpProtocolEvent): Promise { + const params = cloneRecord(event.params) ?? {} + const sequence = ++this.sequence + const record = { + schemaVersion: SESSION_SCHEMA_VERSION, + sequence, + recordedAt: new Date().toISOString(), + method: event.method, + params + } + if (!this.eventFile) throw new Error('Session event journal is not open.') + await this.eventFile.appendFile(`${jsonStringify(record)}\n`, 'utf8') + await this.applyNetworkEvent(event.method, params, sequence) + await this.persistManifest() + } + + private async applyNetworkEvent( + method: string, + params: Record, + sequence: number + ): Promise { + if (!method.startsWith('Network.')) return + const requestId = requestIdFrom(params) + if (!requestId) return + const request = this.ensureRequest(requestId, sequence) + + if (method === 'Network.requestWillBeSent') { + request.requestTimestamp = finiteNumber(params.timestamp) + request.wallTime = finiteNumber(params.wallTime) + request.resourceType = typeof params.type === 'string' ? params.type : request.resourceType + request.request = cloneRecord(params.request) + const trace = traceContextFromHeaders(asRecord(params.request)?.headers) + if (trace) { + request.trace = trace + this.indexTrace(requestId, trace) + } + return + } + + if (method === 'Network.responseReceived') { + request.responseTimestamp = finiteNumber(params.timestamp) + request.resourceType = typeof params.type === 'string' ? params.type : request.resourceType + request.response = cloneRecord(params.response) + return + } + + if (method === 'Network.loadingFailed') { + if (this.terminalRequests.has(requestId)) return + this.terminalRequests.add(requestId) + request.finishedTimestamp = finiteNumber(params.timestamp) + request.failure = { + errorText: + typeof params.errorText === 'string' ? params.errorText : 'Network request failed', + ...(typeof params.canceled === 'boolean' ? { canceled: params.canceled } : {}), + ...(typeof params.blockedReason === 'string' ? { blockedReason: params.blockedReason } : {}) + } + return + } + + if (method !== 'Network.loadingFinished' || this.terminalRequests.has(requestId)) return + this.terminalRequests.add(requestId) + request.finishedTimestamp = finiteNumber(params.timestamp) + request.encodedDataLength = finiteNumber(params.encodedDataLength) + await this.captureBody(requestId, request) + } + + private ensureRequest(requestId: string, sequence: number): SessionRequestIndexEntry { + const existing = this.requestIndex.get(requestId) + if (existing) return existing + const created: SessionRequestIndexEntry = { requestId, firstSequence: sequence } + this.requestIndex.set(requestId, created) + return created + } + + private indexTrace(requestId: string, trace: NonNullable) { + const existing = this.traceIndex.get(trace.traceId) ?? { + traceId: trace.traceId, + requestIds: [], + spans: [] + } + if (!existing.requestIds.includes(requestId)) existing.requestIds.push(requestId) + if (!existing.spans.some((span) => span.requestId === requestId)) { + existing.spans.push({ + requestId, + parentId: trace.parentId, + traceFlags: trace.traceFlags, + sampled: trace.sampled, + traceparent: trace.traceparent, + ...(trace.tracestate ? { tracestate: trace.tracestate } : {}) + }) + } + this.traceIndex.set(trace.traceId, existing) + } + + private async captureBody(requestId: string, request: SessionRequestIndexEntry): Promise { + try { + const result = await this.tap.command( + 'Network.getResponseBody', + { requestId }, + { timeoutMs: this.bodyCommandTimeoutMs } + ) + if (!result || typeof result.body !== 'string' || typeof result.base64Encoded !== 'boolean') { + throw new Error('Network.getResponseBody returned an invalid result.') + } + const body = result.base64Encoded + ? Buffer.from(result.body, 'base64') + : Buffer.from(result.body, 'utf8') + const fileName = bodyFileName(requestId, body) + const relativePath = `bodies/${fileName}` + const absolutePath = resolveInside(this.directory, relativePath) + try { + await fs.writeFile(absolutePath, body, { flag: 'wx' }) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error + const existing = await fs.readFile(absolutePath) + if (sha256(existing) !== sha256(body)) { + throw new Error(`Existing session body does not match its content hash: ${relativePath}`) + } + } + const response = request.response + this.bodyIndex.set(requestId, { + requestId, + path: relativePath, + sha256: sha256(body), + byteLength: body.length, + base64Encoded: result.base64Encoded, + ...(typeof response?.mimeType === 'string' ? { mimeType: response.mimeType } : {}) + }) + } catch (error) { + this.bodyErrors.add(requestId) + this.addIssue('Network.getResponseBody', error, requestId) + } + } + + private addIssue(operation: string, error: unknown, requestId?: string): void { + this.issues.push({ + timestamp: new Date().toISOString(), + operation, + message: messageFor(error), + ...(requestId ? { requestId } : {}) + }) + } + + private buildManifest(): SessionManifest { + return { + ...this.baseManifest, + stats: { + eventCount: this.sequence, + requestCount: this.requestIndex.size, + bodyCount: this.bodyIndex.size, + bodyErrorCount: this.bodyErrors.size, + failedRequestCount: [...this.requestIndex.values()].filter((request) => request.failure) + .length + }, + requestIndex: Object.fromEntries(this.requestIndex), + bodyIndex: Object.fromEntries(this.bodyIndex), + traceIndex: Object.fromEntries(this.traceIndex), + issues: [...this.issues] + } + } + + private persistManifest(): Promise { + return atomicWriteJson(this.manifestPath, this.buildManifest()) + } + + private async finish(): Promise { + this.acceptingEvents = false + this.unsubscribeEvent?.() + this.unsubscribeDisconnect?.() + await this.operationQueue + + if (this.baseManifest.state === 'recording') this.baseManifest.state = 'completed' + this.baseManifest.completedAt = new Date().toISOString() + await this.eventFile?.close() + this.eventFile = undefined + + let finishError = this.fatalError + try { + await this.persistManifest() + } catch (error) { + finishError = finishError ?? (error instanceof Error ? error : new Error(String(error))) + } + if (this.ownsTap) { + try { + await this.tap.close() + } catch (error) { + finishError = finishError ?? (error instanceof Error ? error : new Error(String(error))) + } + } + if (finishError) throw finishError + } +} diff --git a/packages/network-debugger/src/session/trace.test.ts b/packages/network-debugger/src/session/trace.test.ts new file mode 100644 index 0000000..5f5d88f --- /dev/null +++ b/packages/network-debugger/src/session/trace.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from 'vitest' +import { parseTraceparent, traceContextFromHeaders } from './trace' + +describe('traceparent correlation', () => { + test('parses an existing sampled trace context without changing its source headers', () => { + const headers = { + TraceParent: '00-4BF92F3577B34DA6A3CE929D0E0E4736-00F067AA0BA902B7-01', + TraceState: 'vendor=value' + } + const before = JSON.stringify(headers) + + expect(traceContextFromHeaders(headers)).toEqual({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + version: '00', + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + parentId: '00f067aa0ba902b7', + traceFlags: '01', + sampled: true, + tracestate: 'vendor=value' + }) + expect(JSON.stringify(headers)).toBe(before) + }) + + test.each([ + '', + 'ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + '00-00000000000000000000000000000000-00f067aa0ba902b7-01', + '00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01', + '00-short-00f067aa0ba902b7-01', + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra' + ])('rejects invalid traceparent %j', (value) => { + expect(parseTraceparent(value)).toBeUndefined() + }) + + test('reads array header values and reports unsampled flags', () => { + expect( + traceContextFromHeaders({ + traceparent: ['00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00'] + }) + ).toMatchObject({ traceFlags: '00', sampled: false }) + }) +}) diff --git a/packages/network-debugger/src/session/trace.ts b/packages/network-debugger/src/session/trace.ts new file mode 100644 index 0000000..a013288 --- /dev/null +++ b/packages/network-debugger/src/session/trace.ts @@ -0,0 +1,41 @@ +import type { TraceContext } from './types' + +function headerValue(headers: unknown, wantedName: string): string | undefined { + if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return undefined + const wanted = wantedName.toLowerCase() + for (const [name, value] of Object.entries(headers as Record)) { + if (name.toLowerCase() !== wanted) continue + if (Array.isArray(value)) return value.map(String).join(', ') + if (value === undefined || value === null) return undefined + return String(value) + } + return undefined +} + +/** Parse an existing W3C traceparent without modifying request headers. */ +export function parseTraceparent(value: string, tracestate?: string): TraceContext | undefined { + const normalized = value.trim().toLowerCase() + const match = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec(normalized) + if (!match) return undefined + + const [, version, traceId, parentId, traceFlags] = match + if (version === 'ff' || /^0+$/.test(traceId) || /^0+$/.test(parentId)) return undefined + + return { + traceparent: normalized, + version, + traceId, + parentId, + traceFlags, + sampled: (Number.parseInt(traceFlags, 16) & 1) === 1, + ...(tracestate?.trim() ? { tracestate: tracestate.trim() } : {}) + } +} + +export function traceContextFromHeaders(headers: unknown): TraceContext | undefined { + const traceparent = headerValue(headers, 'traceparent') + if (!traceparent) return undefined + return parseTraceparent(traceparent, headerValue(headers, 'tracestate')) +} + +export { headerValue as sessionHeaderValue } diff --git a/packages/network-debugger/src/session/types.ts b/packages/network-debugger/src/session/types.ts new file mode 100644 index 0000000..b27eb56 --- /dev/null +++ b/packages/network-debugger/src/session/types.ts @@ -0,0 +1,226 @@ +import type { DevtoolsTarget } from '../adapters/types' + +export const SESSION_SCHEMA_VERSION = 1 as const + +export type CdpCommandId = number | string + +export interface CdpProtocolEvent> { + method: string + params: T +} + +export interface CdpErrorObject { + code: number + message: string + data?: unknown +} + +export interface ProtocolTapCommandOptions { + timeoutMs?: number +} + +export interface ProtocolTapOptions { + connectTimeoutMs?: number + commandTimeoutMs?: number + closeTimeoutMs?: number + maxPendingCommands?: number +} + +export interface TraceContext { + traceparent: string + version: string + traceId: string + parentId: string + traceFlags: string + sampled: boolean + tracestate?: string +} + +export interface SessionFailure { + errorText: string + canceled?: boolean + blockedReason?: string +} + +export interface SessionRequestIndexEntry { + requestId: string + firstSequence: number + requestTimestamp?: number + wallTime?: number + responseTimestamp?: number + finishedTimestamp?: number + encodedDataLength?: number + resourceType?: string + request?: Record + response?: Record + failure?: SessionFailure + trace?: TraceContext +} + +export interface SessionBodyIndexEntry { + requestId: string + path: string + sha256: string + byteLength: number + base64Encoded: boolean + mimeType?: string +} + +export interface SessionTraceSpan { + requestId: string + parentId: string + traceFlags: string + sampled: boolean + traceparent: string + tracestate?: string +} + +export interface SessionTraceIndexEntry { + traceId: string + requestIds: string[] + spans: SessionTraceSpan[] +} + +export interface SessionRecorderIssue { + timestamp: string + operation: string + message: string + requestId?: string +} + +export interface SessionManifest { + schemaVersion: typeof SESSION_SCHEMA_VERSION + sessionId: string + state: 'recording' | 'completed' | 'failed' + createdAt: string + completedAt?: string + target: DevtoolsTarget + files: { + events: 'events.ndjson' + bodies: 'bodies' + } + stats: { + eventCount: number + requestCount: number + bodyCount: number + bodyErrorCount: number + failedRequestCount: number + } + requestIndex: Record + bodyIndex: Record + traceIndex: Record + issues: SessionRecorderIssue[] +} + +export interface SessionEventRecord> { + schemaVersion: typeof SESSION_SCHEMA_VERSION + sequence: number + recordedAt: string + method: string + params: T +} + +export interface ResponseBodyResult { + body: string + base64Encoded: boolean +} + +export interface SessionProtocolConnection { + readonly target: DevtoolsTarget + readonly state: 'idle' | 'connecting' | 'open' | 'closing' | 'closed' + connect(): Promise + command>( + method: string, + params?: Record, + options?: ProtocolTapCommandOptions + ): Promise + onEvent(listener: (event: CdpProtocolEvent) => void | Promise): () => void + onDisconnect(listener: (error: Error) => void): () => void + close(): Promise +} + +export interface SessionRecorderOptions { + directory: string + target: DevtoolsTarget + tap?: SessionProtocolConnection + bodyCommandTimeoutMs?: number +} + +export interface HarHeader { + name: string + value: string +} + +export interface HarQueryParameter { + name: string + value: string +} + +export interface HarPostData { + mimeType: string + text: string +} + +export interface HarContent { + size: number + mimeType: string + text?: string + encoding?: 'base64' +} + +export interface HarEntry { + startedDateTime: string + time: number + request: { + method: string + url: string + httpVersion: string + cookies: unknown[] + headers: HarHeader[] + queryString: HarQueryParameter[] + postData?: HarPostData + headersSize: number + bodySize: number + } + response: { + status: number + statusText: string + httpVersion: string + cookies: unknown[] + headers: HarHeader[] + content: HarContent + redirectURL: string + headersSize: number + bodySize: number + } + cache: Record + timings: { + blocked: number + dns: number + connect: number + send: number + wait: number + receive: number + ssl: number + } + _requestId: string + _failure?: SessionFailure + _trace?: TraceContext +} + +export interface HarDocument { + log: { + version: '1.2' + creator: { + name: 'node-network-devtools' + version: string + } + pages: unknown[] + entries: HarEntry[] + } +} + +export interface HarExportResult { + outputPath: string + har: HarDocument +} diff --git a/packages/network-debugger/src/target/frontend-launcher.test.ts b/packages/network-debugger/src/target/frontend-launcher.test.ts new file mode 100644 index 0000000..4bf8efb --- /dev/null +++ b/packages/network-debugger/src/target/frontend-launcher.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest' +import type { DevtoolsTarget } from '../adapters/types' + +const open = vi.hoisted(() => vi.fn()) +vi.mock('open', () => ({ default: open })) + +import { openDevtoolsTarget } from './frontend-launcher' + +const target = (overrides: Partial = {}): DevtoolsTarget => ({ + id: 'target', + title: 'Node.js', + type: 'node', + url: 'file:///app.mjs', + webSocketDebuggerUrl: 'ws://127.0.0.1:9229/target', + discoveryUrl: 'http://127.0.0.1:9229/json/list', + devtoolsFrontendUrl: 'devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/target', + ...overrides +}) + +describe('openDevtoolsTarget', () => { + beforeEach(() => { + vi.clearAllMocks() + open.mockResolvedValue({}) + }) + + test('opens the canonical target URL without retaining or waiting for a browser', async () => { + await openDevtoolsTarget(target()) + + expect(open).toHaveBeenCalledWith( + 'devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/target', + { wait: false } + ) + }) + + test('uses the compatibility URL when it is the only frontend descriptor', async () => { + const descriptor = target({ + devtoolsFrontendUrl: undefined, + devtoolsFrontendUrlCompat: 'devtools://compat' + }) + + await openDevtoolsTarget(descriptor) + expect(open).toHaveBeenCalledWith('devtools://compat', { wait: false }) + }) + + test('fails with a stable code when discovery exposes no frontend URL', async () => { + await expect( + openDevtoolsTarget( + target({ + devtoolsFrontendUrl: undefined, + devtoolsFrontendUrlCompat: undefined + }) + ) + ).rejects.toMatchObject({ code: 'NND_FRONTEND_URL_UNAVAILABLE' }) + expect(open).not.toHaveBeenCalled() + }) +}) diff --git a/packages/network-debugger/src/target/frontend-launcher.ts b/packages/network-debugger/src/target/frontend-launcher.ts new file mode 100644 index 0000000..746b965 --- /dev/null +++ b/packages/network-debugger/src/target/frontend-launcher.ts @@ -0,0 +1,16 @@ +import open from 'open' +import type { DevtoolsTarget } from '../adapters/types' + +export async function openDevtoolsTarget(target: DevtoolsTarget): Promise { + const frontendUrl = target.devtoolsFrontendUrl ?? target.devtoolsFrontendUrlCompat + if (!frontendUrl) { + throw Object.assign(new Error('The selected target does not expose a DevTools frontend URL.'), { + code: 'NND_FRONTEND_URL_UNAVAILABLE' + }) + } + + // The browser is deliberately not retained: the target and frontend have + // independent lifecycles, and disposing a registration must never kill a + // user browser process. + await open(frontendUrl, { wait: false }) +} diff --git a/packages/network-debugger/src/utils/file.test.ts b/packages/network-debugger/src/utils/file.test.ts deleted file mode 100644 index 630607a..0000000 --- a/packages/network-debugger/src/utils/file.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import fs from 'fs' -import { unlinkSafe } from './file' - -// Mock fs 模块 -vi.mock('fs') - -describe('file.ts', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('unlinkSafe', () => { - describe('文件存在时的删除操作', () => { - test('成功删除存在的文件', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - const filePath = '/path/to/existing/file.txt' - unlinkSafe(filePath) - - expect(mockUnlinkSync).toHaveBeenCalledTimes(1) - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - - test('删除不同路径的文件', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - const paths = [ - '/tmp/test.txt', - './relative/path/file.log', - 'simple.json', - '/home/user/documents/data.csv' - ] - - paths.forEach((path) => { - unlinkSafe(path) - }) - - expect(mockUnlinkSync).toHaveBeenCalledTimes(4) - paths.forEach((path, index) => { - expect(mockUnlinkSync).toHaveBeenNthCalledWith(index + 1, path) - }) - }) - - test('删除包含特殊字符路径的文件', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - const specialPaths = [ - '/path/with spaces/file.txt', - '/path/with-dashes/file.txt', - '/path/with_underscores/file.txt', - '/path/中文路径/文件.txt' - ] - - specialPaths.forEach((path) => { - unlinkSafe(path) - }) - - expect(mockUnlinkSync).toHaveBeenCalledTimes(4) - }) - }) - - describe('文件不存在时的静默处理', () => { - test('文件不存在时不抛出错误(ENOENT)', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const enoentError = new Error('ENOENT: no such file or directory') - ;(enoentError as NodeJS.ErrnoException).code = 'ENOENT' - mockUnlinkSync.mockImplementation(() => { - throw enoentError - }) - - const filePath = '/path/to/nonexistent/file.txt' - - // 不应该抛出错误 - expect(() => unlinkSafe(filePath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - - test('多次尝试删除不存在的文件都不抛出错误', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const enoentError = new Error('ENOENT: no such file or directory') - ;(enoentError as NodeJS.ErrnoException).code = 'ENOENT' - mockUnlinkSync.mockImplementation(() => { - throw enoentError - }) - - const filePath = '/nonexistent/file.txt' - - // 多次调用都不应该抛出错误 - for (let i = 0; i < 5; i++) { - expect(() => unlinkSafe(filePath)).not.toThrow() - } - - expect(mockUnlinkSync).toHaveBeenCalledTimes(5) - }) - }) - - describe('其他错误的静默处理', () => { - test('权限不足错误时不抛出异常(EACCES)', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const eaccesError = new Error('EACCES: permission denied') - ;(eaccesError as NodeJS.ErrnoException).code = 'EACCES' - mockUnlinkSync.mockImplementation(() => { - throw eaccesError - }) - - const filePath = '/protected/file.txt' - - expect(() => unlinkSafe(filePath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - - test('路径是目录时不抛出异常(EISDIR)', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const eisdirError = new Error('EISDIR: illegal operation on a directory') - ;(eisdirError as NodeJS.ErrnoException).code = 'EISDIR' - mockUnlinkSync.mockImplementation(() => { - throw eisdirError - }) - - const dirPath = '/path/to/directory' - - expect(() => unlinkSafe(dirPath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(dirPath) - }) - - test('文件正在使用时不抛出异常(EBUSY)', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const ebusyError = new Error('EBUSY: resource busy or locked') - ;(ebusyError as NodeJS.ErrnoException).code = 'EBUSY' - mockUnlinkSync.mockImplementation(() => { - throw ebusyError - }) - - const filePath = '/busy/file.txt' - - expect(() => unlinkSafe(filePath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - - test('只读文件系统错误时不抛出异常(EROFS)', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - const erofsError = new Error('EROFS: read-only file system') - ;(erofsError as NodeJS.ErrnoException).code = 'EROFS' - mockUnlinkSync.mockImplementation(() => { - throw erofsError - }) - - const filePath = '/readonly/file.txt' - - expect(() => unlinkSafe(filePath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - - test('通用错误时不抛出异常', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => { - throw new Error('Unknown error') - }) - - const filePath = '/some/file.txt' - - expect(() => unlinkSafe(filePath)).not.toThrow() - expect(mockUnlinkSync).toHaveBeenCalledWith(filePath) - }) - }) - - describe('边界情况', () => { - test('空字符串路径', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - unlinkSafe('') - - expect(mockUnlinkSync).toHaveBeenCalledWith('') - }) - - test('根路径', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - unlinkSafe('/') - - expect(mockUnlinkSync).toHaveBeenCalledWith('/') - }) - - test('相对路径', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - unlinkSafe('./test.txt') - - expect(mockUnlinkSync).toHaveBeenCalledWith('./test.txt') - }) - - test('带有 .. 的路径', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - unlinkSafe('../parent/file.txt') - - expect(mockUnlinkSync).toHaveBeenCalledWith('../parent/file.txt') - }) - - test('Windows 风格路径', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - unlinkSafe('C:\\Users\\test\\file.txt') - - expect(mockUnlinkSync).toHaveBeenCalledWith('C:\\Users\\test\\file.txt') - }) - }) - - describe('返回值', () => { - test('函数返回 undefined', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => undefined) - - const result = unlinkSafe('/path/to/file.txt') - - expect(result).toBeUndefined() - }) - - test('发生错误时也返回 undefined', () => { - const mockUnlinkSync = vi.mocked(fs.unlinkSync) - mockUnlinkSync.mockImplementation(() => { - throw new Error('Some error') - }) - - const result = unlinkSafe('/path/to/file.txt') - - expect(result).toBeUndefined() - }) - }) - }) -}) diff --git a/packages/network-debugger/src/utils/file.ts b/packages/network-debugger/src/utils/file.ts deleted file mode 100644 index f4bb51f..0000000 --- a/packages/network-debugger/src/utils/file.ts +++ /dev/null @@ -1,7 +0,0 @@ -import fs from 'fs' - -export const unlinkSafe = (path: string) => { - try { - fs.unlinkSync(path) - } catch {} -} diff --git a/packages/network-debugger/src/utils/map.ts b/packages/network-debugger/src/utils/map.ts index 192d72e..4f77895 100644 --- a/packages/network-debugger/src/utils/map.ts +++ b/packages/network-debugger/src/utils/map.ts @@ -1,8 +1,15 @@ export function headersToObject(map: T) { - const obj: Record = {}; + const obj: Record = {} map.forEach((value, key) => { - obj[key] = value; - }); + // Header names are user-controlled; define the property directly so the + // legacy `__proto__` setter cannot swallow an otherwise valid entry. + Object.defineProperty(obj, key, { + value, + enumerable: true, + configurable: true, + writable: true + }) + }) - return obj; + return obj } diff --git a/packages/network-debugger/test/e2e/cli/cli.test.mjs b/packages/network-debugger/test/e2e/cli/cli.test.mjs new file mode 100644 index 0000000..62d99a5 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/cli.test.mjs @@ -0,0 +1,540 @@ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import test from 'node:test' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + FIXTURES_DIR, + PACKAGE_DIR, + REGISTER_PATH, + assertCliBuildExists, + atomicReplace, + cleanCliEnvironment, + runCli, + startCli, + temporaryFixtureCopy, + waitForProcessGone +} from './harness.mjs' + +const TEST_TIMEOUT_MS = 30_000 +const CJS_FIXTURE = resolve(FIXTURES_DIR, 'probe.cjs') +const ESM_FIXTURE = resolve(FIXTURES_DIR, 'probe.mjs') +const TS_FIXTURE = resolve(FIXTURES_DIR, 'probe.ts') +const WATCH_FIXTURE = resolve(FIXTURES_DIR, 'watch-entry.mjs') +const NEST_FIXTURE = resolve(FIXTURES_DIR, 'nest-app/dist/main.cjs') +const FRONTEND_OPEN_HOOK = resolve(FIXTURES_DIR, 'frontend-open-hook.cjs') +const FRONTEND_CDP_RUNNER = resolve(FIXTURES_DIR, 'frontend-cdp-runner.mjs') +const FINITE_LEGACY_FIXTURE = resolve(FIXTURES_DIR, 'finite-legacy.cjs') +const WATCH_SUPERVISOR_SETTLE_MS = 500 + +function importedModules(execArgv) { + const modules = [] + for (let index = 0; index < execArgv.length; index += 1) { + const argument = execArgv[index] + if (argument === '--import' && execArgv[index + 1]) { + modules.push(execArgv[index + 1]) + index += 1 + } else if (argument.startsWith('--import=')) { + modules.push(argument.slice('--import='.length)) + } + } + return modules +} + +function importMatchesPath(specifier, expectedPath) { + if (specifier.startsWith('file:')) return fileURLToPath(specifier) === expectedPath + return resolve(specifier) === expectedPath +} + +function assertNativeFixture(record, { label, argv = [] } = {}) { + assert.equal(record.type, 'fixture-ready') + assert.equal(record.label, label) + assert.equal(record.preloadInjected, true) + assert.equal(record.mode, 'native') + assert.deepEqual(record.argv, argv) + assert.ok(Number.isInteger(record.pid) && record.pid > 0) + assert.ok(Number.isInteger(record.ppid) && record.ppid > 0) + + assert.ok( + record.execArgv.includes('--experimental-network-inspection'), + `missing experimental network flag in ${JSON.stringify(record.execArgv)}` + ) + assert.ok( + record.execArgv.some((argument) => argument === '--inspect=127.0.0.1:0'), + `missing ephemeral Inspector flag in ${JSON.stringify(record.execArgv)}` + ) + assert.equal( + record.execArgv.some((argument) => argument.startsWith('--inspect-wait')), + false, + '--no-wait must not inject --inspect-wait' + ) + + const imports = importedModules(record.execArgv) + assert.ok( + imports.some((specifier) => importMatchesPath(specifier, REGISTER_PATH)), + `missing dist/register.mjs preload in ${JSON.stringify(imports)}` + ) + assert.equal(record.capabilities.http, true) + assert.equal(record.capabilities.https, true) + assert.equal(record.capabilities.fetch, true) + assert.equal(record.fallbackReason, null) + assert.match(record.target.webSocketDebuggerUrl, /^ws:\/\/127\.0\.0\.1:\d+\//) + assert.match(record.target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) +} + +function assertNormalExit(result, code = 0) { + assert.equal(result.code, code, `unexpected stderr:\n${result.stderr}`) + assert.equal(result.signal, null) +} + +function parseReadyReport(stderr) { + const prefix = '[nnd:ready] ' + const line = stderr.split(/\r?\n/).find((candidate) => candidate.includes(prefix)) + assert.ok(line, `missing preload readiness report:\n${stderr}`) + return JSON.parse(line.slice(line.indexOf(prefix) + prefix.length)) +} + +async function waitForEndpointClosed(url, { timeoutMs = 5_000 } = {}) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(500) }) + await response.arrayBuffer() + } catch { + return + } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 25)) + } + throw new Error(`Legacy target remained reachable after its application exited: ${url}`) +} + +function runPreloadedNode(execArgv, { timeoutMs = 15_000 } = {}) { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(process.execPath, execArgv, { + cwd: PACKAGE_DIR, + env: cleanCliEnvironment({ + NND_MODE: 'legacy', + NND_LEGACY_SERVER_PORT: '0', + NND_PRELOAD_REPORT: '1' + }), + stdio: ['ignore', 'pipe', 'pipe'] + }) + let stdout = '' + let stderr = '' + let timedOut = false + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + stdout += chunk + }) + child.stderr.on('data', (chunk) => { + stderr += chunk + }) + const timer = setTimeout(() => { + timedOut = true + child.kill('SIGKILL') + }, timeoutMs) + child.once('error', (error) => { + clearTimeout(timer) + rejectPromise(error) + }) + child.once('close', (code, signal) => { + clearTimeout(timer) + if (timedOut) { + rejectPromise( + new Error( + `Preloaded Node process did not exit naturally within ${timeoutMs}ms.\nstdout:\n${stdout}\nstderr:\n${stderr}` + ) + ) + return + } + resolvePromise({ code, signal, stdout, stderr }) + }) + }) +} + +async function assertCliReportedNative(cli, fixtureRecord) { + const { line } = await cli.waitForLine((candidate) => candidate.includes('[nnd:ready] '), { + source: 'stderr', + label: 'CLI Native readiness report' + }) + const ready = JSON.parse(line.slice(line.indexOf('[nnd:ready] ') + '[nnd:ready] '.length)) + assert.equal(ready.mode, 'native') + assert.equal(ready.target.webSocketDebuggerUrl, fixtureRecord.target.webSocketDebuggerUrl) + assert.equal(ready.capabilities.http, true) +} + +function protectCli(t, cli) { + const applicationPids = new Set() + t.after(async () => { + if (cli.running) await cli.terminate().catch(() => {}) + for (const pid of applicationPids) { + try { + process.kill(pid, 'SIGKILL') + } catch (error) { + if (error?.code !== 'ESRCH') throw error + } + } + }) + return applicationPids +} + +test.before(async () => { + await assertCliBuildExists() +}) + +test( + 'doctor --json emits a parseable, versioned and internally consistent report', + { timeout: TEST_TIMEOUT_MS }, + async () => { + const result = await runCli(['doctor', '--json']) + assertNormalExit(result) + assert.equal(result.stderr, '') + + const report = JSON.parse(result.stdout) + assert.equal(report.schemaVersion, 1) + assert.equal(report.ok, true) + assert.equal(report.nodeVersion, process.versions.node) + assert.match(report.packageVersion, /^\d+\.\d+\.\d+/) + assert.equal(report.inspectorAvailable, true) + assert.equal(typeof report.experimentalFlag, 'boolean') + assert.equal(report.native.kind, 'native') + assert.equal(typeof report.native.available, 'boolean') + assert.equal(report.selection.requested, 'auto') + assert.ok(['native', 'legacy'].includes(report.selection.selected)) + assert.equal(report.config.config.inspector.port, 0) + assert.equal(report.config.config.open, false) + assert.ok(Array.isArray(report.diagnostics)) + assert.ok(report.diagnostics.some((item) => item.code === 'NND_DOCTOR_NODE_VERSION')) + assert.ok(report.diagnostics.some((item) => item.code.startsWith('NND_DOCTOR_SELECTED_'))) + } +) + +test( + 'dev --open resumes the real inspect-wait target and opens its authoritative target once', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const directory = await mkdtemp(resolve(tmpdir(), 'node-network-devtools-open-e2e-')) + const frontendRecord = resolve(directory, 'frontend.ndjson') + const cli = startCli(['dev', '--open', ESM_FIXTURE], { + nodeArgs: ['--require', FRONTEND_OPEN_HOOK], + env: cleanCliEnvironment({ + NND_E2E_FRONTEND_RUNNER: FRONTEND_CDP_RUNNER, + NND_E2E_FRONTEND_RECORD: frontendRecord + }) + }) + const applicationPids = protectCli(t, cli) + t.after(() => rm(directory, { recursive: true, force: true })) + + const ready = await cli.waitForRecord((record) => record.label === 'esm') + applicationPids.add(ready.pid) + assert.equal(ready.preloadInjected, true) + assert.equal(ready.mode, 'native') + assert.ok( + ready.execArgv.includes('--inspect-wait=127.0.0.1:0'), + `missing inspect-wait in ${JSON.stringify(ready.execArgv)}` + ) + assert.equal( + ready.execArgv.some((argument) => argument === '--inspect=127.0.0.1:0'), + false + ) + await assertCliReportedNative(cli, ready) + + const result = await cli.waitForExit() + assertNormalExit(result) + await waitForProcessGone(ready.pid) + applicationPids.clear() + + let recordText + const deadline = Date.now() + 5_000 + while (recordText === undefined && Date.now() < deadline) { + try { + recordText = await readFile(frontendRecord, 'utf8') + } catch (error) { + if (error?.code !== 'ENOENT') throw error + await new Promise((resolvePromise) => setTimeout(resolvePromise, 25)) + } + } + assert.ok(recordText, 'frontend CDP runner did not record an opened target') + const opened = recordText + .trim() + .split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)) + assert.equal(opened.length, 1, `authoritative target opened ${opened.length} times`) + assert.equal(opened[0].webSocketDebuggerUrl, ready.target.webSocketDebuggerUrl) + assert.match(opened[0].frontendUrl, /^devtools:\/\//) + } +) + +test( + 'Auto starts Legacy without Native flags when required capabilities cannot be met', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const cli = startCli(['dev', '--no-wait', '--require', 'requestBody', ESM_FIXTURE]) + const applicationPids = protectCli(t, cli) + const ready = await cli.waitForRecord((record) => record.label === 'esm') + applicationPids.add(ready.pid) + + assert.equal(ready.mode, 'legacy') + assert.equal(ready.capabilities.requestBody, true) + assert.equal(ready.fallbackReason?.code, 'NND_AUTO_FALLBACK') + assert.equal(ready.execArgv.includes('--experimental-network-inspection'), false) + assert.equal( + ready.execArgv.some((argument) => argument.startsWith('--inspect')), + false, + `Auto fallback must not create a waiting Native target: ${JSON.stringify(ready.execArgv)}` + ) + + const { line } = await cli.waitForLine((candidate) => candidate.includes('[nnd:ready] '), { + source: 'stderr', + label: 'CLI Legacy fallback readiness report' + }) + const reported = JSON.parse(line.slice(line.indexOf('[nnd:ready] ') + '[nnd:ready] '.length)) + assert.equal(reported.mode, 'legacy') + assert.equal(reported.fallbackReason.code, 'NND_AUTO_FALLBACK') + + const result = await cli.terminate('SIGTERM') + assert.deepEqual(result, { code: 143, signal: null }) + await waitForProcessGone(ready.pid) + applicationPids.clear() + assert.equal( + cli.lines.filter((item) => item.line.includes('[nnd:ready] ')).length, + 1, + 'Legacy bridge descendants must not execute the inherited side-effect preload' + ) + } +) + +test( + 'finite Legacy CLI application exits naturally without accessing the preload handle', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const cli = startCli(['dev', '--mode', 'legacy', '--no-wait', FINITE_LEGACY_FIXTURE]) + const applicationPids = protectCli(t, cli) + const fixture = await cli.waitForRecord((record) => record.type === 'finite-legacy') + applicationPids.add(fixture.pid) + + const { line } = await cli.waitForLine((candidate) => candidate.includes('[nnd:ready] '), { + source: 'stderr', + label: 'finite Legacy readiness report' + }) + const ready = parseReadyReport(line) + assert.equal(ready.mode, 'legacy') + assert.match(ready.target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) + + const result = await cli.waitForExit({ + timeoutMs: 10_000, + label: 'finite Legacy CLI natural exit' + }) + assertNormalExit({ ...result, stderr: cli.stderr }) + await waitForProcessGone(fixture.pid) + applicationPids.clear() + await waitForEndpointClosed(ready.target.discoveryUrl) + assert.equal( + cli.lines.filter((item) => item.line.includes('[nnd:ready] ')).length, + 1, + 'the detached Legacy child must not replay the preload entry' + ) + } +) + +test( + 'direct Legacy preload sanitizes eval and print execArgv and leaves no backend orphan', + { timeout: 75_000 }, + async () => { + const source = + "process.stdout.write('@@NND_E2E@@' + JSON.stringify({ type: 'finite-preload', pid: process.pid, ppid: process.ppid }) + '\\n')" + + for (const option of ['-e', '--eval', '-p', '--print']) { + const result = await runPreloadedNode([`--import=${REGISTER_PATH}`, option, source]) + assertNormalExit(result) + assert.doesNotMatch(result.stderr, /NND_LEGACY_CHILD_START_FAILED/) + const records = result.stdout + .split(/\r?\n/) + .filter((line) => line.startsWith('@@NND_E2E@@')) + .map((line) => JSON.parse(line.slice('@@NND_E2E@@'.length))) + assert.equal(records.length, 1, `${option} evaluation was unexpectedly replayed by a child`) + assert.equal(records[0].type, 'finite-preload') + + const ready = parseReadyReport(result.stderr) + assert.equal(ready.mode, 'legacy') + await waitForProcessGone(records[0].pid) + await waitForEndpointClosed(ready.target.discoveryUrl) + } + } +) + +test( + 'dev --no-wait preloads Native into a CJS entry, forwards args and mirrors exit code', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const applicationArgs = ['alpha', 'value with spaces', '--exit-code=23'] + const cli = startCli(['dev', '--no-wait', CJS_FIXTURE, '--', ...applicationArgs]) + const applicationPids = protectCli(t, cli) + const ready = await cli.waitForRecord((record) => record.label === 'cjs') + applicationPids.add(ready.pid) + assertNativeFixture(ready, { label: 'cjs', argv: applicationArgs }) + await assertCliReportedNative(cli, ready) + + const result = await cli.waitForExit() + assert.equal(result.code, 23, `unexpected CLI output:\n${cli.stdout}\n${cli.stderr}`) + assert.equal(result.signal, null) + await waitForProcessGone(ready.pid) + applicationPids.clear() + } +) + +test( + 'dev --no-wait preloads Native into an ESM entry without source registration', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const applicationArgs = ['--format=esm'] + const cli = startCli(['dev', '--no-wait', ESM_FIXTURE, '--', ...applicationArgs]) + const applicationPids = protectCli(t, cli) + const ready = await cli.waitForRecord((record) => record.label === 'esm') + applicationPids.add(ready.pid) + assertNativeFixture(ready, { label: 'esm', argv: applicationArgs }) + await assertCliReportedNative(cli, ready) + + const result = await cli.waitForExit() + assert.equal(result.code, 0, `unexpected CLI output:\n${cli.stdout}\n${cli.stderr}`) + assert.equal(result.signal, null) + await waitForProcessGone(ready.pid) + applicationPids.clear() + } +) + +test( + 'dev --runner tsx executes a TypeScript entry with both preloads active', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const applicationArgs = ['--typed-argument', '42'] + const cli = startCli([ + 'dev', + '--runner', + 'tsx', + '--no-wait', + TS_FIXTURE, + '--', + ...applicationArgs + ]) + const applicationPids = protectCli(t, cli) + const ready = await cli.waitForRecord((record) => record.label === 'tsx') + applicationPids.add(ready.pid) + assertNativeFixture(ready, { label: 'tsx', argv: applicationArgs }) + await assertCliReportedNative(cli, ready) + assert.equal(ready.typescriptExecuted, true) + assert.ok( + importedModules(ready.execArgv).includes('tsx'), + `missing tsx preload in ${JSON.stringify(ready.execArgv)}` + ) + + const result = await cli.waitForExit() + assert.equal(result.code, 0, `unexpected CLI output:\n${cli.stdout}\n${cli.stderr}`) + assert.equal(result.signal, null) + await waitForProcessGone(ready.pid) + applicationPids.clear() + } +) + +test( + 'dev --watch performs one restart after an atomic entry replacement and leaves no child', + { timeout: 45_000 }, + async (t) => { + const fixture = await temporaryFixtureCopy(WATCH_FIXTURE) + const cli = startCli(['dev', '--watch', '--no-wait', fixture.path]) + const observedPids = new Set() + + t.after(async () => { + if (cli.running) await cli.terminate().catch(() => {}) + for (const pid of observedPids) { + if (Number.isInteger(pid) && pid > 0) { + try { + process.kill(pid, 'SIGKILL') + } catch (error) { + if (error?.code !== 'ESRCH') throw error + } + } + } + await fixture.cleanup() + }) + + const initial = await cli.waitForRecord( + (record) => record.label === 'watch' && record.generation === 'initial-generation' + ) + observedPids.add(initial.pid) + observedPids.add(initial.ppid) + assertNativeFixture(initial, { label: 'watch' }) + + // Node's native watch supervisor has no public "watcher armed" signal. + // The worker can print readiness before the supervisor consumes its + // watch:import IPC and creates the directory watcher, so an immediate + // atomic editor-style replacement can otherwise fall into that window. + await new Promise((resolvePromise) => setTimeout(resolvePromise, WATCH_SUPERVISOR_SETTLE_MS)) + await atomicReplace(fixture.path, async () => { + const source = await readFile(fixture.path, 'utf8') + return source.replace('initial-generation', 'updated-generation') + }) + + const restarted = await cli.waitForRecord( + (record) => + record.label === 'watch' && + record.generation === 'updated-generation' && + record.pid !== initial.pid, + { timeoutMs: 25_000, label: 'watch restart readiness' } + ) + observedPids.add(restarted.pid) + observedPids.add(restarted.ppid) + assertNativeFixture(restarted, { label: 'watch' }) + assert.notEqual(restarted.pid, initial.pid) + assert.equal(restarted.ppid, initial.ppid, 'Node watch supervisor must remain stable') + await waitForProcessGone(initial.pid) + + const result = await cli.terminate('SIGTERM') + assert.equal(result.signal, null) + assert.ok( + result.code === 0 || result.code === 143, + `watch supervisor must exit cleanly or preserve SIGTERM semantics; received ${result.code}` + ) + await waitForProcessGone(restarted.pid) + await waitForProcessGone(restarted.ppid) + observedPids.clear() + } +) + +test( + 'compiled Nest-style bootstrap needs no registration edit and receives forwarded SIGTERM', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + const cli = startCli(['dev', '--no-wait', NEST_FIXTURE]) + let appPid + + t.after(async () => { + if (cli.running) await cli.terminate().catch(() => {}) + if (appPid) { + try { + process.kill(appPid, 'SIGKILL') + } catch (error) { + if (error?.code !== 'ESRCH') throw error + } + } + }) + + const ready = await cli.waitForRecord((record) => record.label === 'nest-compiled') + appPid = ready.pid + assertNativeFixture(ready, { label: 'nest-compiled' }) + assert.equal(ready.framework, 'nest-style') + assert.equal(ready.module, 'AppModule') + assert.equal(ready.listenHost, '127.0.0.1') + assert.ok(Number.isInteger(ready.listenPort) && ready.listenPort > 0) + + const result = await cli.terminate('SIGTERM') + assert.deepEqual(result, { code: 143, signal: null }) + await waitForProcessGone(appPid) + appPid = undefined + } +) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/finite-legacy.cjs b/packages/network-debugger/test/e2e/cli/fixtures/finite-legacy.cjs new file mode 100644 index 0000000..1ef9116 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/finite-legacy.cjs @@ -0,0 +1,12 @@ +'use strict' + +// Deliberately do not read the preload state symbol or dispose its internal +// registration handle. A debugging preload must never extend application +// lifetime; the bridge child owns cleanup after the parent IPC disconnects. +process.stdout.write( + `@@NND_E2E@@${JSON.stringify({ + type: 'finite-legacy', + pid: process.pid, + ppid: process.ppid + })}\n` +) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/frontend-cdp-runner.mjs b/packages/network-debugger/test/e2e/cli/fixtures/frontend-cdp-runner.mjs new file mode 100644 index 0000000..4a046cd --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/frontend-cdp-runner.mjs @@ -0,0 +1,48 @@ +import { appendFile } from 'node:fs/promises' +import WebSocket from 'ws' + +const frontendUrl = process.argv[2] +const recordPath = process.env.NND_E2E_FRONTEND_RECORD +if (!frontendUrl || !recordPath) { + throw new Error('frontend URL and NND_E2E_FRONTEND_RECORD are required') +} + +const frontend = new URL(frontendUrl) +const address = frontend.searchParams.get('ws') +if (!address) throw new Error(`Frontend URL has no ws parameter: ${frontendUrl}`) +const webSocketDebuggerUrl = address.startsWith('ws:') ? address : `ws://${address}` + +await new Promise((resolvePromise, rejectPromise) => { + const socket = new WebSocket(webSocketDebuggerUrl) + const timeout = setTimeout(() => { + socket.terminate() + rejectPromise(new Error(`Timed out resuming ${webSocketDebuggerUrl}`)) + }, 10_000) + + const cleanup = () => clearTimeout(timeout) + socket.once('error', (error) => { + cleanup() + rejectPromise(error) + }) + socket.once('open', () => { + socket.send(JSON.stringify({ id: 1, method: 'Runtime.runIfWaitingForDebugger' })) + }) + socket.on('message', async (data) => { + const message = JSON.parse(data.toString()) + if (message.id !== 1) return + try { + await appendFile( + recordPath, + `${JSON.stringify({ frontendUrl, webSocketDebuggerUrl })}\n`, + 'utf8' + ) + cleanup() + socket.close() + resolvePromise() + } catch (error) { + cleanup() + socket.terminate() + rejectPromise(error) + } + }) +}) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/frontend-open-hook.cjs b/packages/network-debugger/test/e2e/cli/fixtures/frontend-open-hook.cjs new file mode 100644 index 0000000..64ffadd --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/frontend-open-hook.cjs @@ -0,0 +1,43 @@ +'use strict' + +const childProcess = require('node:child_process') +const { syncBuiltinESMExports } = require('node:module') + +const originalSpawn = childProcess.spawn +const runnerPath = process.env.NND_E2E_FRONTEND_RUNNER + +function extractFrontendUrl(argumentsList) { + for (const argument of argumentsList ?? []) { + if (typeof argument !== 'string') continue + const directIndex = argument.indexOf('devtools://') + if (directIndex >= 0) return argument.slice(directIndex).replace(/["']+$/, '') + + // `open` uses a UTF-16LE encoded PowerShell command on Windows. + try { + const decoded = Buffer.from(argument, 'base64').toString('utf16le') + const match = decoded.match(/devtools:\/\/[^"'\s]+/) + if (match) return match[0] + } catch { + // Not a base64 PowerShell argument. + } + } + return undefined +} + +childProcess.spawn = function nndE2eSpawn(command, argumentsList, options) { + const frontendUrl = extractFrontendUrl(argumentsList) + if (!frontendUrl) return originalSpawn.call(this, command, argumentsList, options) + if (!runnerPath) throw new Error('NND_E2E_FRONTEND_RUNNER is required') + + return originalSpawn(process.execPath, [runnerPath, frontendUrl], { + env: options?.env ?? process.env, + // The real launcher is detached on Linux. Preserve that lifecycle while + // avoiding Windows-only spawn flags when the replacement executable is Node. + detached: options?.detached ?? false, + stdio: options?.stdio ?? 'ignore' + }) +} + +// The CLI imports spawn from `node:child_process`; synchronize the patched CJS +// export before its ESM graph is evaluated. +syncBuiltinESMExports() diff --git a/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/app.module.cjs b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/app.module.cjs new file mode 100644 index 0000000..39494d7 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/app.module.cjs @@ -0,0 +1,9 @@ +'use strict' + +// This is deliberately shaped like emitted TypeScript from a Nest application: +// the CLI must preload instrumentation into compiled output without source edits. +Object.defineProperty(exports, '__esModule', { value: true }) +exports.AppModule = void 0 + +class AppModule {} +exports.AppModule = AppModule diff --git a/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/main.cjs b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/main.cjs new file mode 100644 index 0000000..1a13131 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/main.cjs @@ -0,0 +1,20 @@ +'use strict' + +Object.defineProperty(exports, '__esModule', { value: true }) + +const { AppModule } = require('./app.module.cjs') +const { NestFactory } = require('./nest-factory.cjs') +const { fail, reportReady } = require('../../probe-core.cjs') + +async function bootstrap() { + const app = await NestFactory.create(AppModule) + const address = await app.listen(0, '127.0.0.1') + await reportReady('nest-compiled', { + framework: 'nest-style', + module: AppModule.name, + listenHost: address.address, + listenPort: address.port + }) +} + +void bootstrap().catch(fail) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/nest-factory.cjs b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/nest-factory.cjs new file mode 100644 index 0000000..b5878ff --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/nest-app/dist/nest-factory.cjs @@ -0,0 +1,23 @@ +'use strict' + +const http = require('node:http') + +exports.NestFactory = { + async create(AppModule) { + const module = new AppModule() + const server = http.createServer((_request, response) => { + response.writeHead(200, { 'content-type': 'application/json' }) + response.end(JSON.stringify({ framework: 'nest-style', module: module.constructor.name })) + }) + + return { + async listen(port, host) { + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(port, host, resolve) + }) + return server.address() + } + } + } +} diff --git a/packages/network-debugger/test/e2e/cli/fixtures/probe-core.cjs b/packages/network-debugger/test/e2e/cli/fixtures/probe-core.cjs new file mode 100644 index 0000000..51d5ce6 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/probe-core.cjs @@ -0,0 +1,53 @@ +'use strict' + +const RECORD_PREFIX = '@@NND_E2E@@' +const PRELOAD_STATE = Symbol.for('node-network-devtools.preload.state') + +async function reportReady(label, extra = {}) { + const preloadState = globalThis[PRELOAD_STATE] + if (!preloadState?.promise) { + throw new Error('NND preload state is unavailable; the CLI did not inject dist/register.mjs') + } + + const handle = await preloadState.promise + const ready = await handle.ready + const record = { + type: 'fixture-ready', + label, + pid: process.pid, + ppid: process.ppid, + argv: process.argv.slice(2), + execArgv: [...process.execArgv], + preloadInjected: true, + mode: ready.mode, + capabilities: ready.capabilities, + fallbackReason: ready.fallbackReason ?? null, + target: { + id: ready.target.id, + type: ready.target.type, + discoveryUrl: ready.target.discoveryUrl, + webSocketDebuggerUrl: ready.target.webSocketDebuggerUrl + }, + ...extra + } + process.stdout.write(`${RECORD_PREFIX}${JSON.stringify(record)}\n`) + return { handle, ready, record } +} + +function exitCodeFromArgv() { + const value = process.argv.slice(2).find((argument) => argument.startsWith('--exit-code=')) + if (!value) return 0 + const code = Number(value.slice('--exit-code='.length)) + if (!Number.isInteger(code) || code < 0 || code > 255) { + throw new Error(`Invalid fixture exit code: ${value}`) + } + return code +} + +function fail(error) { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error) + process.stderr.write(`@@NND_E2E_ERROR@@${message}\n`) + process.exitCode = 70 +} + +module.exports = { exitCodeFromArgv, fail, reportReady } diff --git a/packages/network-debugger/test/e2e/cli/fixtures/probe.cjs b/packages/network-debugger/test/e2e/cli/fixtures/probe.cjs new file mode 100644 index 0000000..9df03a2 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/probe.cjs @@ -0,0 +1,10 @@ +'use strict' + +const { exitCodeFromArgv, fail, reportReady } = require('./probe-core.cjs') + +void reportReady('cjs') + .then(async ({ handle }) => { + process.exitCode = exitCodeFromArgv() + await handle.dispose() + }) + .catch(fail) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/probe.mjs b/packages/network-debugger/test/e2e/cli/fixtures/probe.mjs new file mode 100644 index 0000000..de09b17 --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/probe.mjs @@ -0,0 +1,9 @@ +import probe from './probe-core.cjs' + +try { + const { handle } = await probe.reportReady('esm') + process.exitCode = probe.exitCodeFromArgv() + await handle.dispose() +} catch (error) { + probe.fail(error) +} diff --git a/packages/network-debugger/test/e2e/cli/fixtures/probe.ts b/packages/network-debugger/test/e2e/cli/fixtures/probe.ts new file mode 100644 index 0000000..3e18f9d --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/probe.ts @@ -0,0 +1,53 @@ +type PreloadState = { + promise: Promise<{ + ready: Promise<{ + mode: string + capabilities: Record + fallbackReason?: unknown + target: { + id: string + type: string + discoveryUrl: string + webSocketDebuggerUrl: string + } + }> + dispose(): Promise + }> +} + +const RECORD_PREFIX: string = '@@NND_E2E@@' +const PRELOAD_STATE: symbol = Symbol.for('node-network-devtools.preload.state') + +async function main(): Promise { + const preloadState = (globalThis as Record)[PRELOAD_STATE] as + | PreloadState + | undefined + if (!preloadState?.promise) { + throw new Error('NND preload state is unavailable; the CLI did not inject dist/register.mjs') + } + + const handle = await preloadState.promise + const ready = await handle.ready + const record = { + type: 'fixture-ready', + label: 'tsx', + pid: process.pid, + ppid: process.ppid, + argv: process.argv.slice(2), + execArgv: [...process.execArgv], + preloadInjected: true, + mode: ready.mode, + capabilities: ready.capabilities, + fallbackReason: ready.fallbackReason ?? null, + target: ready.target, + typescriptExecuted: true + } + process.stdout.write(`${RECORD_PREFIX}${JSON.stringify(record)}\n`) + await handle.dispose() +} + +void main().catch((error: unknown) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error) + process.stderr.write(`@@NND_E2E_ERROR@@${message}\n`) + process.exitCode = 70 +}) diff --git a/packages/network-debugger/test/e2e/cli/fixtures/watch-entry.mjs b/packages/network-debugger/test/e2e/cli/fixtures/watch-entry.mjs new file mode 100644 index 0000000..86c32ea --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/fixtures/watch-entry.mjs @@ -0,0 +1,29 @@ +const RECORD_PREFIX = '@@NND_E2E@@' +const PRELOAD_STATE = Symbol.for('node-network-devtools.preload.state') +const generation = 'initial-generation' + +const preloadState = globalThis[PRELOAD_STATE] +if (!preloadState?.promise) { + throw new Error('NND preload state is unavailable; the CLI did not inject dist/register.mjs') +} + +const handle = await preloadState.promise +const ready = await handle.ready +process.stdout.write( + `${RECORD_PREFIX}${JSON.stringify({ + type: 'fixture-ready', + label: 'watch', + generation, + pid: process.pid, + ppid: process.ppid, + argv: process.argv.slice(2), + execArgv: [...process.execArgv], + preloadInjected: true, + mode: ready.mode, + capabilities: ready.capabilities, + fallbackReason: ready.fallbackReason ?? null, + target: ready.target + })}\n` +) + +setInterval(() => {}, 60_000) diff --git a/packages/network-debugger/test/e2e/cli/harness.mjs b/packages/network-debugger/test/e2e/cli/harness.mjs new file mode 100644 index 0000000..2adae1a --- /dev/null +++ b/packages/network-debugger/test/e2e/cli/harness.mjs @@ -0,0 +1,365 @@ +import { spawn } from 'node:child_process' +import { EventEmitter } from 'node:events' +import { access, copyFile, mkdtemp, rename, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { randomUUID } from 'node:crypto' + +const CLI_DIR = dirname(fileURLToPath(import.meta.url)) + +export const PACKAGE_DIR = resolve(CLI_DIR, '../../..') +export const CLI_PATH = resolve(PACKAGE_DIR, 'dist/cli.mjs') +export const REGISTER_PATH = resolve(PACKAGE_DIR, 'dist/register.mjs') +export const FIXTURES_DIR = resolve(CLI_DIR, 'fixtures') +export const FIXTURE_RECORD_PREFIX = '@@NND_E2E@@' + +const DEFAULT_TIMEOUT_MS = 15_000 + +function timeoutError(label, timeoutMs, processState) { + return new Error( + `Timed out after ${timeoutMs}ms while waiting for ${label}.\n` + + `CLI state: ${JSON.stringify(processState)}\n` + ) +} + +function parseFixtureRecord(line) { + const marker = line.indexOf(FIXTURE_RECORD_PREFIX) + if (marker === -1) return undefined + + const serialized = line.slice(marker + FIXTURE_RECORD_PREFIX.length).trim() + try { + return JSON.parse(serialized) + } catch (error) { + throw new Error(`Fixture emitted invalid JSON: ${serialized}`, { cause: error }) + } +} + +export async function assertCliBuildExists() { + await Promise.all( + [CLI_PATH, REGISTER_PATH].map((path) => + access(path).catch(() => { + throw new Error(`Missing built CLI artifact ${path}. Run the package build before CLI E2E.`) + }) + ) + ) +} + +export function cleanCliEnvironment(overrides = {}) { + const environment = { ...process.env } + for (const key of Object.keys(environment)) { + if (key.startsWith('NND_')) delete environment[key] + } + delete environment.NODE_OPTIONS + return { ...environment, ...overrides } +} + +export class CliProcess { + constructor( + args, + { + cwd = PACKAGE_DIR, + env = cleanCliEnvironment(), + stdio = ['ignore', 'pipe', 'pipe'], + nodeArgs = [] + } = {} + ) { + this.args = [...args] + this.cwd = cwd + this.stdout = '' + this.stderr = '' + this.lines = [] + this.records = [] + this.events = new EventEmitter() + this.closed = undefined + + const child = spawn(process.execPath, [...nodeArgs, CLI_PATH, ...args], { + cwd, + env, + stdio + }) + this.child = child + this.#capture(child.stdout, 'stdout') + this.#capture(child.stderr, 'stderr') + child.once('error', (error) => this.events.emit('process-error', error)) + child.once('close', (code, signal) => { + this.closed = { code, signal } + this.events.emit('process-close', this.closed) + }) + } + + get pid() { + return this.child.pid + } + + get running() { + return ( + this.closed === undefined && this.child.exitCode === null && this.child.signalCode === null + ) + } + + output() { + return { stdout: this.stdout, stderr: this.stderr } + } + + state() { + return { + args: this.args, + cwd: this.cwd, + pid: this.pid, + exitCode: this.child.exitCode, + signalCode: this.child.signalCode, + stdout: this.stdout, + stderr: this.stderr + } + } + + waitForLine(predicate, { source, timeoutMs = DEFAULT_TIMEOUT_MS, label = 'CLI output' } = {}) { + const existing = this.lines.find( + (item) => (!source || item.source === source) && predicate(item.line, item) + ) + if (existing) return Promise.resolve(existing) + + if (this.closed) { + return Promise.reject( + new Error(`CLI exited before ${label}: ${JSON.stringify(this.closed)}\n${this.stderr}`) + ) + } + + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(timeoutError(label, timeoutMs, this.state())) + }, timeoutMs) + + const onLine = (item) => { + if (source && item.source !== source) return + let matches + try { + matches = predicate(item.line, item) + } catch (error) { + cleanup() + reject(error) + return + } + if (!matches) return + cleanup() + resolvePromise(item) + } + const onError = (error) => { + cleanup() + reject(error) + } + const onClose = (result) => { + cleanup() + reject(new Error(`CLI exited before ${label}: ${JSON.stringify(result)}\n${this.stderr}`)) + } + const cleanup = () => { + clearTimeout(timer) + this.events.off('line', onLine) + this.events.off('process-error', onError) + this.events.off('process-close', onClose) + } + + this.events.on('line', onLine) + this.events.once('process-error', onError) + this.events.once('process-close', onClose) + }) + } + + waitForRecord(predicate = () => true, options = {}) { + const existing = this.records.find(predicate) + if (existing) return Promise.resolve(existing) + + if (this.closed) { + return Promise.reject( + new Error( + `CLI exited before fixture readiness: ${JSON.stringify(this.closed)}\n${this.stderr}` + ) + ) + } + + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS + const label = options.label ?? 'fixture readiness record' + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(timeoutError(label, timeoutMs, this.state())) + }, timeoutMs) + const onRecord = (record) => { + let matches + try { + matches = predicate(record) + } catch (error) { + cleanup() + reject(error) + return + } + if (!matches) return + cleanup() + resolvePromise(record) + } + const onError = (error) => { + cleanup() + reject(error) + } + const onClose = (result) => { + cleanup() + reject( + new Error( + `CLI exited before ${label}: ${JSON.stringify(result)}\nstdout:\n${this.stdout}\nstderr:\n${this.stderr}` + ) + ) + } + const cleanup = () => { + clearTimeout(timer) + this.events.off('record', onRecord) + this.events.off('process-error', onError) + this.events.off('process-close', onClose) + } + + this.events.on('record', onRecord) + this.events.once('process-error', onError) + this.events.once('process-close', onClose) + }) + } + + waitForExit({ timeoutMs = DEFAULT_TIMEOUT_MS, label = 'CLI exit' } = {}) { + if (this.closed) return Promise.resolve(this.closed) + + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(timeoutError(label, timeoutMs, this.state())) + }, timeoutMs) + const onClose = (result) => { + cleanup() + resolvePromise(result) + } + const onError = (error) => { + cleanup() + reject(error) + } + const cleanup = () => { + clearTimeout(timer) + this.events.off('process-close', onClose) + this.events.off('process-error', onError) + } + + this.events.once('process-close', onClose) + this.events.once('process-error', onError) + }) + } + + async terminate(signal = 'SIGTERM', { timeoutMs = 5_000 } = {}) { + if (!this.running) return this.waitForExit({ timeoutMs, label: 'already-ended CLI' }) + + this.child.kill(signal) + try { + return await this.waitForExit({ timeoutMs, label: `CLI to handle ${signal}` }) + } catch (error) { + if (this.running) this.child.kill('SIGKILL') + await this.waitForExit({ timeoutMs: 2_000, label: 'CLI forced termination' }).catch(() => {}) + throw error + } + } + + #capture(stream, source) { + if (!stream) return + stream.setEncoding('utf8') + let pending = '' + + const emitLine = (line) => { + const item = { source, line } + this.lines.push(item) + this.events.emit('line', item) + + let record + try { + record = parseFixtureRecord(line) + } catch (error) { + this.events.emit('process-error', error) + return + } + if (record === undefined) return + this.records.push(record) + this.events.emit('record', record) + } + + stream.on('data', (chunk) => { + this[source] += chunk + pending += chunk + const lines = pending.split(/\r?\n/) + pending = lines.pop() ?? '' + lines.forEach(emitLine) + }) + stream.on('end', () => { + if (pending) emitLine(pending) + }) + } +} + +export function startCli(args, options) { + return new CliProcess(args, options) +} + +export async function runCli(args, options = {}) { + const cli = startCli(args, options) + try { + const result = await cli.waitForExit({ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS }) + return { ...result, ...cli.output(), records: [...cli.records] } + } catch (error) { + if (cli.running) await cli.terminate().catch(() => {}) + throw error + } +} + +export function processExists(pid) { + if (!Number.isInteger(pid) || pid <= 0) return false + try { + process.kill(pid, 0) + return true + } catch (error) { + if (error?.code === 'ESRCH') return false + if (error?.code === 'EPERM') return true + throw error + } +} + +export async function waitForProcessGone(pid, { timeoutMs = 5_000 } = {}) { + if (!processExists(pid)) return + + await new Promise((resolvePromise, reject) => { + const deadline = Date.now() + timeoutMs + const check = () => { + if (!processExists(pid)) { + resolvePromise() + return + } + if (Date.now() >= deadline) { + reject(new Error(`Process ${pid} remained alive for more than ${timeoutMs}ms.`)) + return + } + setTimeout(check, 25) + } + check() + }) +} + +export async function atomicReplace(path, transform) { + const temporary = `${path}.${process.pid}.${randomUUID()}.tmp` + const next = await transform() + await writeFile(temporary, next, 'utf8') + await rename(temporary, path) +} + +export async function temporaryFixtureCopy(sourcePath) { + const directory = await mkdtemp(join(tmpdir(), 'node-network-devtools-cli-e2e-')) + const targetPath = join(directory, sourcePath.split(/[\\/]/).at(-1)) + await copyFile(sourcePath, targetPath) + return { + directory, + path: targetPath, + cleanup: () => rm(directory, { recursive: true, force: true }) + } +} diff --git a/packages/network-debugger/test/e2e/enhancements/README.md b/packages/network-debugger/test/e2e/enhancements/README.md new file mode 100644 index 0000000..b27fa86 --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/README.md @@ -0,0 +1,28 @@ +# Phase 6 enhancements E2E + +This suite is a black-box acceptance gate for Session, HAR, Replay, Legacy Mock, +and trace correlation. It loads the built ESM package, starts a real Legacy CDP +target, connects both a raw CDP client and `SessionRecorder`, and sends business +traffic to a real loopback origin. Process IPC is used only for test control. + +Run it after building the package: + +```sh +pnpm --filter node-network-devtools build +node --test packages/network-debugger/test/e2e/enhancements/enhancements.test.mjs +``` + +The suite verifies: + +- `manifest.json`, ordered `events.ndjson`, content-addressed body files, and + body integrity hashes; +- HAR 1.2 text and binary bodies plus POST data; +- Session dry-run planning and real HAR replay against the loopback origin; +- real `http.request` and Fetch calls intercepted by Legacy Mock without origin + leakage; +- correlation of an existing `traceparent`/`tracestate` and absence of implicit + trace-header injection; and +- the stable Native-plus-Mock conflict through a separate built consumer. + +On failure, the harness prints a temporary artifact directory containing the +session, raw CDP journal, consumer logs, IPC messages, and origin request log. diff --git a/packages/network-debugger/test/e2e/enhancements/enhancements-harness.mjs b/packages/network-debugger/test/e2e/enhancements/enhancements-harness.mjs new file mode 100644 index 0000000..c0d12b3 --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/enhancements-harness.mjs @@ -0,0 +1,318 @@ +import { spawn } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { CdpClient } from '../harness/cdp-client.mjs' +import { startEnhancementsOrigin } from './fixtures/origin-server.mjs' + +const DIRECTORY = dirname(fileURLToPath(import.meta.url)) +const PACKAGE_DIRECTORY = resolve(DIRECTORY, '../../..') +const DEFAULT_TIMEOUT_MS = 15_000 + +function waitForExit(child, timeoutMs, label) { + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.resolve({ code: child.exitCode, signal: child.signalCode }) + } + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${label}`)) + }, timeoutMs) + const onExit = (code, signal) => { + cleanup() + resolvePromise({ code, signal }) + } + const cleanup = () => { + clearTimeout(timer) + child.off('exit', onExit) + } + child.once('exit', onExit) + }) +} + +async function fetchJson(url) { + const response = await fetch(url, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) }) + const text = await response.text() + if (!response.ok) throw new Error(`HTTP ${response.status} from ${url}: ${text}`) + return JSON.parse(text) +} + +export class EnhancementsHarness { + constructor() { + this.entryPath = + process.env.NETWORK_DEBUGGER_E2E_ENTRY_ESM ?? resolve(PACKAGE_DIRECTORY, 'dist/index.mjs') + this.fixturePath = resolve(DIRECTORY, 'fixtures/consumer.mjs') + this.messages = [] + this.stdout = '' + this.stderr = '' + this.closed = false + this.keepArtifacts = false + } + + async start() { + await access(this.entryPath).catch(() => { + throw new Error( + `Built package entry is missing at ${this.entryPath}. ` + + 'Run `pnpm --filter node-network-devtools build` before this suite.' + ) + }) + this.temporaryDirectory = await mkdtemp( + join(tmpdir(), 'node-network-devtools-enhancements-e2e-') + ) + this.sessionDirectory = join(this.temporaryDirectory, 'session') + this.origin = await startEnhancementsOrigin() + + const child = spawn(process.execPath, [this.fixturePath], { + cwd: PACKAGE_DIRECTORY, + env: { + ...process.env, + NETWORK_DEBUGGER_E2E_ENTRY: this.entryPath, + NETWORK_DEBUGGER_E2E_ORIGIN: this.origin.baseUrl, + NETWORK_DEBUGGER_E2E_SESSION_DIR: this.sessionDirectory + }, + stdio: ['ignore', 'pipe', 'pipe', 'ipc'] + }) + this.child = child + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (this.stdout += chunk)) + child.stderr.on('data', (chunk) => (this.stderr += chunk)) + child.on('message', (message) => this.messages.push(message)) + + const ready = await this.waitForMessage((message) => message?.type === 'ready') + this.targetPid = ready.pid + this.sessionInfo = ready.sessionInfo + if (this.sessionInfo?.mode !== 'legacy') { + throw new Error(`Enhancements consumer selected ${this.sessionInfo?.mode ?? 'no backend'}`) + } + const target = this.sessionInfo.target + if (!/^ws:\/\/127\.0\.0\.1:\d+\//.test(target?.webSocketDebuggerUrl ?? '')) { + throw new Error(`Expected a real loopback CDP target: ${JSON.stringify(target)}`) + } + + const discovery = await fetchJson(target.discoveryUrl) + if (!Array.isArray(discovery) || !discovery.some((item) => item.id === target.id)) { + throw new Error(`Target ${target.id} is absent from /json/list`) + } + this.discovery = discovery + + this.client = new CdpClient(target.webSocketDebuggerUrl) + await this.client.connect() + this.networkEnable = await this.client.command('Network.enable') + return this + } + + createToken(prefix) { + return `${prefix}-${randomUUID()}` + } + + url(pathname, token) { + const url = new URL(pathname, this.origin.baseUrl) + url.searchParams.set('token', token) + return url.href + } + + command(type, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const id = randomUUID() + const response = this.waitForMessage( + (message) => message?.type === 'command-result' && message.id === id, + { timeoutMs } + ) + return new Promise((resolvePromise, reject) => { + this.child.send({ type, id, ...payload }, (error) => { + if (error) reject(error) + else resolvePromise() + }) + }) + .then(() => response) + .then((message) => { + if (!message.ok) throw new Error(`Enhancements command ${type} failed:\n${message.error}`) + return message.result + }) + } + + run(scenario, payload = {}) { + return this.command('run', { scenario, ...payload }) + } + + finalize() { + return this.command('finalize', {}, { timeoutMs: 30_000 }) + } + + waitForMessage(predicate, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const existing = this.messages.find(predicate) + if (existing) return Promise.resolve(existing) + if (this.child?.exitCode !== null || this.child?.signalCode !== null) { + return Promise.reject( + new Error( + `Enhancements consumer already exited ` + + `(code=${this.child?.exitCode}, signal=${this.child?.signalCode}).\n${this.stderr}` + ) + ) + } + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject( + new Error( + `Timed out after ${timeoutMs}ms waiting for consumer IPC. stderr:\n${this.stderr}` + ) + ) + }, timeoutMs) + const onMessage = (message) => { + if (!predicate(message)) return + cleanup() + resolvePromise(message) + } + const onExit = (code, signal) => { + cleanup() + reject( + new Error( + `Enhancements consumer exited while waiting for IPC ` + + `(code=${code}, signal=${signal}).\n${this.stderr}` + ) + ) + } + const cleanup = () => { + clearTimeout(timer) + this.child.off('message', onMessage) + this.child.off('exit', onExit) + } + this.child.on('message', onMessage) + this.child.once('exit', onExit) + }) + } + + async preserveFailure(error) { + this.keepArtifacts = true + if (!this.temporaryDirectory) return undefined + await mkdir(this.temporaryDirectory, { recursive: true }) + await Promise.all([ + this.client?.writeJournal(join(this.temporaryDirectory, 'raw-cdp.ndjson')), + writeFile(join(this.temporaryDirectory, 'consumer.stdout.log'), this.stdout, 'utf8'), + writeFile(join(this.temporaryDirectory, 'consumer.stderr.log'), this.stderr, 'utf8'), + writeFile( + join(this.temporaryDirectory, 'consumer-messages.ndjson'), + this.messages.map((message) => JSON.stringify(message)).join('\n') + '\n', + 'utf8' + ), + writeFile( + join(this.temporaryDirectory, 'origin.ndjson'), + (this.origin?.records ?? []).map((record) => JSON.stringify(record)).join('\n') + '\n', + 'utf8' + ), + writeFile( + join(this.temporaryDirectory, 'failure.json'), + JSON.stringify( + { + error: error instanceof Error ? { message: error.message, stack: error.stack } : error, + sessionInfo: this.sessionInfo, + node: process.version, + platform: process.platform + }, + null, + 2 + ), + 'utf8' + ) + ]) + return this.temporaryDirectory + } + + async close() { + if (this.closed) return + this.closed = true + const errors = [] + + try { + await this.client?.close() + } catch (error) { + errors.push(error) + } + + if (this.child?.connected && this.child.exitCode === null && this.child.signalCode === null) { + try { + await this.command('shutdown', {}, { timeoutMs: 6_000 }) + } catch (error) { + errors.push(error) + } + } + + if (this.child && this.child.exitCode === null && this.child.signalCode === null) { + try { + await waitForExit(this.child, 3_000, 'enhancements consumer to exit') + } catch (error) { + errors.push(error) + this.child.kill('SIGTERM') + try { + await waitForExit(this.child, 2_000, 'enhancements consumer to terminate') + } catch (terminationError) { + errors.push(terminationError) + this.child.kill('SIGKILL') + } + } + } + + try { + await this.origin?.close() + } catch (error) { + errors.push(error) + } + + if (errors.length) this.keepArtifacts = true + if (!this.keepArtifacts && this.temporaryDirectory) { + await rm(this.temporaryDirectory, { recursive: true, force: true }) + } + if (errors.length) { + throw new AggregateError(errors, 'Failed to clean up enhancements E2E harness') + } + } +} + +export async function withEnhancementsHarness(testContext, callback) { + const harness = new EnhancementsHarness() + testContext.after(async () => { + try { + await harness.close() + } catch (error) { + const directory = await harness.preserveFailure(error).catch(() => undefined) + if (directory) console.error(`Enhancements cleanup failure artifacts: ${directory}`) + throw error + } + }) + + try { + await harness.start() + return await callback(harness) + } catch (error) { + const directory = await harness.preserveFailure(error).catch(() => undefined) + if (directory) { + console.error(`Enhancements E2E failure artifacts: ${directory}`) + if (error instanceof Error) error.message += `\nFailure artifacts: ${directory}` + } + throw error + } +} + +export async function runNativeMockConflictConsumer() { + const entryPath = + process.env.NETWORK_DEBUGGER_E2E_ENTRY_ESM ?? resolve(PACKAGE_DIRECTORY, 'dist/index.mjs') + await access(entryPath) + const fixturePath = resolve(DIRECTORY, 'fixtures/native-mock-conflict.mjs') + const child = spawn(process.execPath, [fixturePath], { + cwd: PACKAGE_DIRECTORY, + env: { ...process.env, NETWORK_DEBUGGER_E2E_ENTRY: entryPath }, + stdio: ['ignore', 'pipe', 'pipe'] + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (stdout += chunk)) + child.stderr.on('data', (chunk) => (stderr += chunk)) + const exit = await waitForExit(child, DEFAULT_TIMEOUT_MS, 'Native plus Mock consumer') + return { ...exit, stdout, stderr } +} diff --git a/packages/network-debugger/test/e2e/enhancements/enhancements.test.mjs b/packages/network-debugger/test/e2e/enhancements/enhancements.test.mjs new file mode 100644 index 0000000..e9c85aa --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/enhancements.test.mjs @@ -0,0 +1,272 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import test from 'node:test' + +import { runNativeMockConflictConsumer, withEnhancementsHarness } from './enhancements-harness.mjs' +import { BINARY_BODY } from './fixtures/origin-server.mjs' + +const TEST_TIMEOUT_MS = 60_000 +const TRACEPARENT = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' +const TRACESTATE = 'vendor=value' + +function entryForUrl(har, url) { + const entry = har.log.entries.find((candidate) => candidate.request.url === url) + assert.ok(entry, `HAR entry is missing for ${url}`) + return entry +} + +function indexedRequestForUrl(manifest, url) { + const entry = Object.values(manifest.requestIndex).find( + (candidate) => candidate.request?.url === url + ) + assert.ok(entry, `Session request index is missing ${url}`) + return entry +} + +test( + 'records real Legacy traffic, exports HAR, replays, mocks, and correlates trace context', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withEnhancementsHarness(t, async (harness) => { + assert.equal(harness.networkEnable.request.id, harness.networkEnable.response.id) + assert.deepEqual(harness.networkEnable.result, {}) + + const traceToken = harness.createToken('trace') + const binaryToken = harness.createToken('binary') + const plainToken = harness.createToken('plain') + const postToken = harness.createToken('post') + const mockHttpToken = harness.createToken('mock-http') + const mockFetchToken = harness.createToken('mock-fetch') + const urls = { + trace: harness.url('/text', traceToken), + binary: harness.url('/binary', binaryToken), + plain: harness.url('/plain', plainToken), + post: harness.url('/post', postToken), + mockHttp: harness.url('/mock-http', mockHttpToken), + mockFetch: harness.url('/mock-fetch', mockFetchToken) + } + const postBody = `replay-post:${postToken}:正文` + + const traceResult = await harness.run('http-text', { + url: urls.trace, + headers: { traceparent: TRACEPARENT, tracestate: TRACESTATE } + }) + const binaryResult = await harness.run('http-binary', { url: urls.binary }) + const plainResult = await harness.run('http-plain', { url: urls.plain }) + const postResult = await harness.run('fetch-post', { + url: urls.post, + headers: { 'content-type': 'text/plain; charset=utf-8' }, + body: postBody + }) + const mockHttpResult = await harness.run('mock-http', { url: urls.mockHttp }) + const mockFetchResult = await harness.run('mock-fetch', { + url: urls.mockFetch, + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'x-mock-match': 'fetch' + }, + body: 'mock-fetch-request' + }) + + assert.equal(traceResult.result.status, 200) + assert.equal( + Buffer.from(traceResult.result.bodyBase64, 'base64').toString('utf8'), + `session-text:${traceToken}:你好` + ) + assert.deepEqual(Buffer.from(binaryResult.result.bodyBase64, 'base64'), BINARY_BODY) + assert.equal(plainResult.result.status, 200) + assert.equal(postResult.result.status, 201) + assert.equal(JSON.parse(postResult.result.body).requestBody, postBody) + assert.equal(mockHttpResult.result.status, 207) + assert.equal(mockHttpResult.result.headers['x-mock-kind'], 'http') + assert.equal( + Buffer.from(mockHttpResult.result.bodyBase64, 'base64').toString('utf8'), + 'legacy-http-mock-body' + ) + assert.equal(mockFetchResult.result.status, 202) + assert.equal(mockFetchResult.result.headers['x-mock-kind'], 'fetch') + assert.equal(mockFetchResult.result.body, 'legacy-fetch-mock-body') + + for (const result of [ + traceResult, + binaryResult, + plainResult, + postResult, + mockHttpResult, + mockFetchResult + ]) { + assert.equal(result.terminalMethod, 'Network.loadingFinished') + assert.equal(typeof result.requestId, 'string') + assert.ok(result.requestId.length > 0) + } + for (const url of Object.values(urls)) { + assert.equal( + harness.client.findEvents( + 'Network.requestWillBeSent', + (params) => params.request?.url === url + ).length, + 1, + `independent raw CDP client did not observe ${url}` + ) + } + + // Mocked calls are real http.request/fetch invocations, but the loopback + // origin must prove neither call escaped the Legacy mock layer. + assert.equal( + harness.origin.records.some( + (record) => record.pathname === '/mock-http' || record.pathname === '/mock-fetch' + ), + false + ) + + const finalized = await harness.finalize() + const manifest = JSON.parse( + await readFile(resolve(harness.sessionDirectory, 'manifest.json'), 'utf8') + ) + const eventText = await readFile(resolve(harness.sessionDirectory, 'events.ndjson'), 'utf8') + const events = eventText + .trim() + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)) + const har = JSON.parse(await readFile(finalized.harPath, 'utf8')) + + assert.equal(manifest.schemaVersion, 1) + assert.equal(manifest.state, 'completed') + assert.equal(manifest.target.id, harness.sessionInfo.target.id) + assert.equal(manifest.stats.requestCount, 6) + assert.equal(manifest.stats.bodyCount, 6) + assert.equal(manifest.stats.bodyErrorCount, 0) + assert.equal(manifest.stats.failedRequestCount, 0) + assert.equal(manifest.issues.length, 0) + assert.equal(events.length, manifest.stats.eventCount) + assert.ok(events.length >= 18) + events.forEach((event, index) => { + assert.equal(event.schemaVersion, 1) + assert.equal(event.sequence, index + 1) + assert.equal(typeof event.recordedAt, 'string') + }) + assert.equal( + events.some( + (event) => + event.method === 'Network.requestWillBeSent' && + event.params?.request?.url === urls.trace + ), + true + ) + assert.equal( + events.some((event) => event.method.startsWith('Network.webSocket')), + false, + 'the Session tap must not record its own CDP WebSocket transport' + ) + assert.equal( + Object.values(manifest.requestIndex).some((request) => + request.request?.url?.startsWith('ws://127.0.0.1:') + ), + false, + 'the Session tap must not appear in the request index' + ) + + for (const bodyIndex of Object.values(manifest.bodyIndex)) { + const bytes = await readFile(resolve(harness.sessionDirectory, bodyIndex.path)) + assert.equal(bytes.byteLength, bodyIndex.byteLength) + assert.equal(createHash('sha256').update(bytes).digest('hex'), bodyIndex.sha256) + } + + assert.equal(har.log.version, '1.2') + assert.equal(har.log.entries.length, 6) + const textEntry = entryForUrl(har, urls.trace) + assert.equal(textEntry.response.content.text, `session-text:${traceToken}:你好`) + assert.equal(textEntry.response.content.encoding, undefined) + const binaryEntry = entryForUrl(har, urls.binary) + assert.equal(binaryEntry.response.content.encoding, 'base64') + assert.deepEqual(Buffer.from(binaryEntry.response.content.text, 'base64'), BINARY_BODY) + const postEntry = entryForUrl(har, urls.post) + assert.equal(postEntry.request.method, 'POST') + assert.equal(postEntry.request.postData.text, postBody) + assert.equal(entryForUrl(har, urls.mockHttp).response.status, 207) + assert.equal(entryForUrl(har, urls.mockFetch).response.status, 202) + + const tracedRequest = indexedRequestForUrl(manifest, urls.trace) + assert.deepEqual(tracedRequest.trace, { + traceparent: TRACEPARENT, + version: '00', + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + parentId: '00f067aa0ba902b7', + traceFlags: '01', + sampled: true, + tracestate: TRACESTATE + }) + assert.deepEqual(manifest.traceIndex[tracedRequest.trace.traceId].requestIds, [ + tracedRequest.requestId + ]) + assert.equal(indexedRequestForUrl(manifest, urls.plain).trace, undefined) + assert.equal(entryForUrl(har, urls.trace)._trace.traceparent, TRACEPARENT) + assert.equal(entryForUrl(har, urls.plain)._trace, undefined) + + assert.equal(finalized.dryRun.dryRun, true) + assert.equal(finalized.dryRun.requests.length, 6) + assert.equal(finalized.dryRun.succeeded, 6) + assert.equal(finalized.dryRun.failed, 0) + assert.equal( + finalized.dryRun.results.every((result) => result.dryRun && result.ok), + true + ) + assert.equal(finalized.realReplay.dryRun, false) + assert.equal(finalized.realReplay.requests.length, 6) + assert.equal(finalized.realReplay.succeeded, 6) + assert.equal(finalized.realReplay.failed, 0) + assert.equal( + finalized.realReplay.results.every((result) => !result.dryRun && result.ok), + true + ) + + // Four non-mocked requests hit once while recording and once during real + // HAR replay. Header evidence proves trace correlation did not inject a + // traceparent into unrelated requests. + for (const token of [traceToken, binaryToken, plainToken, postToken]) { + assert.equal( + harness.origin.records.filter((record) => record.token === token).length, + 2, + `expected original plus replay traffic for ${token}` + ) + } + assert.equal( + harness.origin.records.some( + (record) => record.pathname === '/mock-http' || record.pathname === '/mock-fetch' + ), + false + ) + for (const record of harness.origin.records.filter((record) => record.token === traceToken)) { + assert.equal(record.headers.traceparent, TRACEPARENT) + assert.equal(record.headers.tracestate, TRACESTATE) + } + for (const record of harness.origin.records.filter((record) => record.token === plainToken)) { + assert.equal(record.headers.traceparent, undefined) + assert.equal(record.headers.tracestate, undefined) + } + for (const record of harness.origin.records.filter((record) => record.token === postToken)) { + assert.equal(Buffer.from(record.bodyBase64, 'base64').toString('utf8'), postBody) + } + }) + } +) + +test( + 'built consumer rejects Native plus Legacy Mock with a stable conflict', + { timeout: TEST_TIMEOUT_MS }, + async () => { + const result = await runNativeMockConflictConsumer() + assert.equal(result.code, 0, result.stderr) + assert.equal(result.signal, null) + const lines = result.stdout.trim().split('\n').filter(Boolean) + const error = JSON.parse(lines.at(-1)) + assert.equal(error.name, 'RuntimeRegistrationError') + assert.equal(error.code, 'NND_NATIVE_MOCK_CONFLICT') + assert.match(error.message, /only with the Legacy backend/i) + assert.equal(error.details.mode, 'native') + assert.equal(error.details.mockRuleCount, 1) + } +) diff --git a/packages/network-debugger/test/e2e/enhancements/fixtures/consumer.mjs b/packages/network-debugger/test/e2e/enhancements/fixtures/consumer.mjs new file mode 100644 index 0000000..253d91c --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/fixtures/consumer.mjs @@ -0,0 +1,202 @@ +import http from 'node:http' +import { pathToFileURL } from 'node:url' + +const entryPath = process.env.NETWORK_DEBUGGER_E2E_ENTRY +const originBaseUrl = process.env.NETWORK_DEBUGGER_E2E_ORIGIN +const sessionDirectory = process.env.NETWORK_DEBUGGER_E2E_SESSION_DIR + +if (!entryPath) throw new Error('NETWORK_DEBUGGER_E2E_ENTRY is required') +if (!originBaseUrl) throw new Error('NETWORK_DEBUGGER_E2E_ORIGIN is required') +if (!sessionDirectory) throw new Error('NETWORK_DEBUGGER_E2E_SESSION_DIR is required') + +const api = await import(pathToFileURL(entryPath).href) +for (const name of ['register', 'SessionRecorder', 'exportHar', 'replay']) { + if (api[name] === undefined) throw new Error(`No public ${name} export found at ${entryPath}`) +} + +const mockRules = [ + { + id: 'e2e-http-mock', + match: { url: `${originBaseUrl}/mock-http*`, method: 'GET' }, + response: { + status: 207, + statusText: 'Mock HTTP', + headers: { 'content-type': 'text/plain; charset=utf-8', 'x-mock-kind': 'http' }, + body: 'legacy-http-mock-body' + } + }, + { + id: 'e2e-fetch-mock', + match: { + url: `${originBaseUrl}/mock-fetch*`, + method: 'POST', + headers: { 'x-mock-match': 'fetch' } + }, + response: { + status: 202, + statusText: 'Mock Fetch', + headers: { 'content-type': 'text/plain; charset=utf-8', 'x-mock-kind': 'fetch' }, + body: 'legacy-fetch-mock-body' + } + } +] + +const registration = api.register({ + mode: 'legacy', + devtools: { open: false }, + legacy: { serverPort: 0, mock: mockRules } +}) +const ready = await registration.ready +if (ready.mode !== 'legacy') throw new Error(`Expected Legacy, received ${ready.mode}`) + +const recorder = await api.SessionRecorder.start({ + directory: sessionDirectory, + target: ready.target +}) + +function send(message) { + return new Promise((resolve, reject) => { + if (!process.send) { + reject(new Error('Enhancements consumer lost its IPC channel')) + return + } + process.send(message, (error) => (error ? reject(error) : resolve())) + }) +} + +function httpRequest(url, { method = 'GET', headers = {}, body } = {}) { + return new Promise((resolve, reject) => { + const request = http.request(url, { method, headers }, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolve({ + status: response.statusCode, + statusText: response.statusMessage, + headers: response.headers, + bodyBase64: Buffer.concat(chunks).toString('base64') + }) + }) + }) + request.once('error', reject) + request.end(body) + }) +} + +function waitForTerminal(url, operation, timeoutMs = 10_000) { + let requestId + let unsubscribe = () => {} + let timer + const terminal = new Promise((resolve, reject) => { + timer = setTimeout(() => { + unsubscribe() + reject(new Error(`Timed out waiting for a recorded lifecycle: ${url}`)) + }, timeoutMs) + unsubscribe = recorder.tap.onEvent((event) => { + if (event.method === 'Network.requestWillBeSent' && event.params?.request?.url === url) { + requestId = event.params.requestId + return + } + if (!requestId || event.params?.requestId !== requestId) return + if (event.method === 'Network.loadingFailed') { + clearTimeout(timer) + unsubscribe() + reject( + new Error( + `Recorded request failed for ${url}: ${event.params?.errorText ?? 'unknown error'}` + ) + ) + } else if (event.method === 'Network.loadingFinished') { + clearTimeout(timer) + unsubscribe() + resolve({ requestId, terminalMethod: event.method }) + } + }) + }) + + return Promise.all([Promise.resolve().then(operation), terminal]).then(([result, lifecycle]) => ({ + result, + ...lifecycle + })) +} + +async function runScenario(message) { + const url = message.url + switch (message.scenario) { + case 'http-text': + case 'http-binary': + case 'http-plain': + case 'mock-http': + return waitForTerminal(url, () => httpRequest(url, { headers: message.headers ?? {} })) + case 'fetch-post': + case 'mock-fetch': + return waitForTerminal(url, async () => { + const response = await fetch(url, { + method: 'POST', + headers: message.headers ?? {}, + body: message.body + }) + return { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers), + body: await response.text() + } + }) + default: + throw new Error(`Unknown enhancements E2E scenario: ${message.scenario}`) + } +} + +let finalized = false +async function finalizeSession() { + if (finalized) throw new Error('Enhancements session was already finalized') + finalized = true + await recorder.close() + const exported = await api.exportHar(sessionDirectory) + const dryRun = await api.replay(sessionDirectory, { dryRun: true }) + const realReplay = await api.replay(exported.outputPath, { timeoutMs: 5_000 }) + return { + manifest: recorder.getManifest(), + harPath: exported.outputPath, + dryRun, + realReplay + } +} + +async function shutdown() { + if (!finalized) await recorder.close() + await registration.dispose() +} + +if (!process.send) throw new Error('Enhancements consumer must be started with process IPC') + +const onMessage = async (message) => { + if (!message || typeof message !== 'object' || typeof message.id !== 'string') return + try { + let result + if (message.type === 'run') result = await runScenario(message) + else if (message.type === 'finalize') result = await finalizeSession() + else if (message.type === 'shutdown') { + process.off('message', onMessage) + await shutdown() + await send({ type: 'command-result', id: message.id, ok: true, result: {} }) + process.disconnect() + return + } else { + throw new Error(`Unknown enhancements E2E command: ${message.type}`) + } + await send({ type: 'command-result', id: message.id, ok: true, result }) + } catch (error) { + await send({ + type: 'command-result', + id: message.id, + ok: false, + error: error instanceof Error ? error.stack : String(error) + }) + } +} + +process.on('message', onMessage) +await send({ type: 'ready', pid: process.pid, sessionInfo: ready }) diff --git a/packages/network-debugger/test/e2e/enhancements/fixtures/native-mock-conflict.mjs b/packages/network-debugger/test/e2e/enhancements/fixtures/native-mock-conflict.mjs new file mode 100644 index 0000000..928ab4c --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/fixtures/native-mock-conflict.mjs @@ -0,0 +1,28 @@ +import { pathToFileURL } from 'node:url' + +const entryPath = process.env.NETWORK_DEBUGGER_E2E_ENTRY +if (!entryPath) throw new Error('NETWORK_DEBUGGER_E2E_ENTRY is required') + +const { register } = await import(pathToFileURL(entryPath).href) +try { + register({ + mode: 'native', + legacy: { + mock: [{ match: { url: 'http://127.0.0.1/*' }, response: { body: 'nope' } }] + } + }) + throw new Error('Native plus Mock unexpectedly registered') +} catch (error) { + const result = { + name: error?.name, + code: error?.code, + message: error?.message, + details: error?.details + } + if (result.code !== 'NND_NATIVE_MOCK_CONFLICT') { + console.error(JSON.stringify(result)) + process.exitCode = 1 + } else { + console.log(JSON.stringify(result)) + } +} diff --git a/packages/network-debugger/test/e2e/enhancements/fixtures/origin-server.mjs b/packages/network-debugger/test/e2e/enhancements/fixtures/origin-server.mjs new file mode 100644 index 0000000..6fe9b14 --- /dev/null +++ b/packages/network-debugger/test/e2e/enhancements/fixtures/origin-server.mjs @@ -0,0 +1,124 @@ +import http from 'node:http' + +export const BINARY_BODY = Buffer.from([0x00, 0x01, 0x02, 0x7f, 0x80, 0xfe, 0xff, 0x42]) + +async function readBody(request) { + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + return Buffer.concat(chunks) +} + +function closeServer(server, sockets) { + return new Promise((resolve, reject) => { + server.close((error) => { + if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error) + else resolve() + }) + for (const socket of sockets) socket.destroy() + }) +} + +/** A real loopback origin. IPC remains the test-control plane. */ +export async function startEnhancementsOrigin() { + const sockets = new Set() + const records = [] + + const server = http.createServer(async (request, response) => { + const url = new URL(request.url, `http://${request.headers.host ?? '127.0.0.1'}`) + const body = await readBody(request) + const token = url.searchParams.get('token') ?? '' + records.push({ + sequence: records.length + 1, + method: request.method, + url: url.href, + pathname: url.pathname, + token, + headers: { ...request.headers }, + bodyBase64: body.toString('base64') + }) + + if (url.pathname === '/text') { + const result = `session-text:${token}:你好` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(result), + 'x-origin': 'enhancements' + }) + response.end(result) + return + } + + if (url.pathname === '/binary') { + response.writeHead(200, { + 'content-type': 'application/octet-stream', + 'content-length': BINARY_BODY.length, + 'x-origin': 'enhancements' + }) + response.end(BINARY_BODY) + return + } + + if (url.pathname === '/plain') { + const result = `plain-response:${token}` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(result), + 'x-origin': 'enhancements' + }) + response.end(result) + return + } + + if (url.pathname === '/post') { + const result = JSON.stringify({ token, requestBody: body.toString('utf8') }) + response.writeHead(201, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(result), + 'x-origin': 'enhancements' + }) + response.end(result) + return + } + + // These paths are deliberately real and observable. A matching Legacy + // mock must prevent them from ever reaching this handler. + if (url.pathname === '/mock-http' || url.pathname === '/mock-fetch') { + response.writeHead(418, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('mock unexpectedly reached the origin') + return + } + + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('not found') + }) + + server.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => sockets.delete(socket)) + }) + + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + + const address = server.address() + if (!address || typeof address === 'string') { + await closeServer(server, sockets) + throw new Error('Enhancements origin did not expose a TCP address') + } + + let closed = false + return { + baseUrl: `http://127.0.0.1:${address.port}`, + records, + async close() { + if (closed) return + closed = true + await closeServer(server, sockets) + } + } +} diff --git a/packages/network-debugger/test/e2e/fixtures/native-target.mjs b/packages/network-debugger/test/e2e/fixtures/native-target.mjs new file mode 100644 index 0000000..b10c8b4 --- /dev/null +++ b/packages/network-debugger/test/e2e/fixtures/native-target.mjs @@ -0,0 +1,159 @@ +import http from 'node:http' +import https from 'node:https' + +const entryUrl = process.env.NETWORK_DEBUGGER_E2E_ENTRY +if (!entryUrl) throw new Error('NETWORK_DEBUGGER_E2E_ENTRY is required') +if (!process.send) throw new Error('Native E2E target must be started with an IPC channel') + +const originalNetworkFunctions = { + fetch: globalThis.fetch, + httpRequest: http.request, + httpsRequest: https.request +} + +const { register } = await import(entryUrl) +if (typeof register !== 'function') + throw new Error(`No public register() export found at ${entryUrl}`) + +const handle = register({ + mode: 'native', + autoOpenDevtool: false, + ...(process.env.NETWORK_DEBUGGER_E2E_SESSION_DIR + ? { + session: { + directory: process.env.NETWORK_DEBUGGER_E2E_SESSION_DIR, + har: true + } + } + : {}) +}) + +if (typeof handle !== 'function' || !handle.ready || typeof handle.dispose !== 'function') { + throw new Error('register({ mode: "native" }) did not return the public callable session handle') +} + +const sessionInfo = await handle.ready +if (sessionInfo.mode !== 'native') { + throw new Error(`Expected native adapter, received ${sessionInfo.mode}`) +} + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => (error ? reject(error) : resolve())) + }) +} + +function performHttpGet(url, token) { + return new Promise((resolve, reject) => { + const request = http.get(url, { headers: { 'x-e2e-token': token } }, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolve({ + status: response.statusCode, + body: Buffer.concat(chunks).toString('utf8') + }) + }) + }) + request.once('error', reject) + }) +} + +async function performFetchPost(url, token) { + const requestBody = `fetch-request:${token}` + const request = new Request(url, { + method: 'POST', + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'x-e2e-token': token + }, + body: requestBody + }) + const response = await fetch(request) + return { + status: response.status, + body: await response.text(), + requestBody + } +} + +async function performFailedRequest(url, token) { + try { + await performHttpGet(url, token) + } catch (error) { + return { failed: true, error: String(error) } + } + throw new Error('Expected the reset endpoint to fail the HTTP request') +} + +async function runScenario(message) { + switch (message.scenario) { + case 'http': + return performHttpGet(message.url, message.token) + case 'fetch': + return performFetchPost(message.url, message.token) + case 'failed': + return performFailedRequest(message.url, message.token) + default: + throw new Error(`Unknown E2E scenario: ${message.scenario}`) + } +} + +process.on('message', async (message) => { + if (message?.type === 'run') { + try { + const result = await runScenario(message) + await send({ type: 'scenario-result', id: message.id, ok: true, result }) + } catch (error) { + await send({ + type: 'scenario-result', + id: message.id, + ok: false, + error: error instanceof Error ? error.stack : String(error) + }) + } + return + } + + if (message?.type === 'finalize-session') { + try { + await handle.dispose() + await send({ + type: 'session-finalized', + id: message.id, + ok: true, + result: sessionInfo.session + }) + } catch (error) { + await send({ + type: 'session-finalized', + id: message.id, + ok: false, + error: error instanceof Error ? error.stack : String(error) + }) + } + return + } + + if (message?.type === 'shutdown') { + try { + await handle.dispose() + await send({ type: 'shutdown-complete' }) + } finally { + process.removeAllListeners('message') + process.disconnect() + } + } +}) + +await send({ + type: 'ready', + pid: process.pid, + sessionInfo, + nativeFunctionsUnchanged: { + fetch: globalThis.fetch === originalNetworkFunctions.fetch, + httpRequest: http.request === originalNetworkFunctions.httpRequest, + httpsRequest: https.request === originalNetworkFunctions.httpsRequest + } +}) diff --git a/packages/network-debugger/test/e2e/fixtures/origin-server.mjs b/packages/network-debugger/test/e2e/fixtures/origin-server.mjs new file mode 100644 index 0000000..984d2c5 --- /dev/null +++ b/packages/network-debugger/test/e2e/fixtures/origin-server.mjs @@ -0,0 +1,82 @@ +import http from 'node:http' + +async function readRequestBody(request) { + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + return Buffer.concat(chunks).toString('utf8') +} + +function closeServer(server, sockets) { + return new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())) + for (const socket of sockets) socket.destroy() + }) +} + +export async function startOriginServer() { + const sockets = new Set() + const server = http.createServer(async (request, response) => { + const url = new URL(request.url, 'http://127.0.0.1') + const token = url.searchParams.get('token') ?? '' + + try { + if (url.pathname === '/http') { + const body = `http-response:${token}` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-e2e-token': token + }) + response.end(body) + return + } + + if (url.pathname === '/fetch') { + const requestBody = await readRequestBody(request) + const body = JSON.stringify({ token, requestBody }) + response.writeHead(201, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-e2e-token': token + }) + response.end(body) + return + } + + if (url.pathname === '/reset') { + request.socket.destroy(new Error(`intentional reset for ${token}`)) + return + } + + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('not found') + } catch (error) { + if (!response.headersSent) response.writeHead(500) + response.end(String(error)) + } + }) + + server.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => sockets.delete(socket)) + }) + + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + + const address = server.address() + if (!address || typeof address === 'string') { + await closeServer(server, sockets) + throw new Error('Origin fixture did not expose a TCP address') + } + + return { + baseUrl: `http://127.0.0.1:${address.port}`, + close: () => closeServer(server, sockets) + } +} diff --git a/packages/network-debugger/test/e2e/frontend/README.md b/packages/network-debugger/test/e2e/frontend/README.md new file mode 100644 index 0000000..6cd05f9 --- /dev/null +++ b/packages/network-debugger/test/e2e/frontend/README.md @@ -0,0 +1,76 @@ +# Official DevTools frontend smoke + +This test uses the official DevTools frontend bundled with the Chromium revision pinned by +`@playwright/test@1.62.1` (Playwright Chromium build 1234 / Chrome for Testing 151.0.7922.34). +The Chromium remote-debugging server hosts that exact build locally, and both Native and Legacy +smokes open: + +```text +http:///devtools/js_app.html?experiments=true&v8only=true&ws=&hl=en-US +``` + +The browser serves `/devtools/js_app.html`, its generated entrypoints, and model modules itself; +there is no copied or third-party frontend. Chromium is launched with external networking disabled +and the frontend, target, and origin all use bundled assets or `127.0.0.1`, so the test requires no +internet access. +The test also records frontend resource requests and WebSockets and fails if any use a non-loopback +host. + +The pinned frontend model API used by the smoke is deliberately small: + +```js +const Logs = await import('./models/logs/logs.js') +const requests = Logs.NetworkLog.NetworkLog.instance().requests() +const content = await request.requestContentData() + +const SDK = await import('./core/sdk/sdk.js') +const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget() +``` + +Each smoke performs the request/body assertion twice: once after the initial connection, then again +after reloading the frontend and confirming that it recreated one active `node` target. Both +requests carry different generated tokens. The Legacy smoke starts the built public package with +forced Legacy and `serverPort: 0`, connects through its standard discovered target, and keeps two +additional raw CDP clients connected. Those clients deliberately issue different commands with the +same numeric command id, proving that a successful body response and an unknown-method error are +returned only to their issuing connections while the official frontend also requests the body. + +`chrome-devtools-frontend@1.0.1684555` was evaluated but cannot be served directly: its npm +tarball contains TypeScript sources and `front_end/entrypoint_template.html`, but no generated +`js_app.html`, JavaScript entrypoint, or generated CSS modules. Building it requires Chromium's +GN/depot_tools toolchain. The Playwright Chromium bundle is therefore the reproducible runnable +frontend pin for this test; an old third-party prebuilt package must not be substituted. + +Run after building the package and installing Playwright Chromium: + +```sh +pnpm --filter node-network-devtools build +pnpm exec playwright install chromium +pnpm exec playwright test -c test/e2e/frontend/playwright.config.mjs +``` + +Run only the forced-Legacy smoke with: + +```sh +pnpm exec playwright test -c test/e2e/frontend/playwright.config.mjs legacy-frontend.spec.mjs +``` + +Suggested package script: + +```json +"test:e2e:frontend": "playwright test -c test/e2e/frontend/playwright.config.mjs" +``` + +CI should install the pinned browser before the offline test phase with +`pnpm exec playwright install --with-deps chromium`, build `node-network-devtools`, run this script +on the Node version that provides `--experimental-network-inspection`, and upload +`NETWORK_DEBUGGER_E2E_ARTIFACT_DIR` with `if: always()`. + +The Legacy smoke attaches a full-page screenshot, Playwright trace, browser/frontend logs, metadata, +and both raw-client CDP journals on success. On failure both smokes attach the same frontend evidence +plus target logs and the primary CDP NDJSON journal. + +The Chromium asset server has no `/favicon.ico`; that one exact local 404 is ignored. Known failures +of optional Network commands not implemented by Node Native are allowlisted only for the Native +smoke. Legacy owns its CDP implementation and has no protocol-error allowlist. Every other frontend +console error, page error, renderer crash, or non-loopback connection fails either smoke. diff --git a/packages/network-debugger/test/e2e/frontend/chromium-devtools.mjs b/packages/network-debugger/test/e2e/frontend/chromium-devtools.mjs new file mode 100644 index 0000000..b05daac --- /dev/null +++ b/packages/network-debugger/test/e2e/frontend/chromium-devtools.mjs @@ -0,0 +1,485 @@ +import { spawn } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { chromium } from '@playwright/test' + +const DEFAULT_TIMEOUT_MS = 10_000 + +// Node's native Network backend does not implement these optional Chromium +// commands yet. The official frontend reports them through console.error while +// continuing to operate normally; every other frontend error remains fatal. +const EXPECTED_PROTOCOL_ERRORS = { + native: [ + /^Request Network\.(?:configureDurableMessages|setAttachDebugStack|setBlockedURLs|emulateNetworkConditionsByRule|overrideNetworkState|clearAcceptedEncodingsOverride) failed\./ + ], + // Legacy owns its CDP implementation, so frontend compatibility errors are + // product failures rather than runtime limitations. + legacy: [] +} + +function waitForExit(child, timeoutMs = 5_000) { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve() + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Chromium did not exit within ${timeoutMs}ms`)) + }, timeoutMs) + const onExit = () => { + cleanup() + resolve() + } + const cleanup = () => { + clearTimeout(timer) + child.off('exit', onExit) + } + child.once('exit', onExit) + if (child.exitCode !== null || child.signalCode !== null) onExit() + }) +} + +function waitForBrowserWebSocket(child, output, timeoutMs = DEFAULT_TIMEOUT_MS) { + const current = output.stderr.match(/DevTools listening on (ws:\/\/[^\s]+)/)?.[1] + if (current) return Promise.resolve(current) + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Timed out waiting for Chromium CDP endpoint. stderr:\n${output.stderr}`)) + }, timeoutMs) + + const onData = () => { + const match = output.stderr.match(/DevTools listening on (ws:\/\/[^\s]+)/) + if (!match) return + cleanup() + resolve(match[1]) + } + const onExit = (code, signal) => { + cleanup() + reject( + new Error( + `Chromium exited before publishing CDP (code=${code}, signal=${signal}).\n${output.stderr}` + ) + ) + } + const onError = (error) => { + cleanup() + reject(error) + } + const cleanup = () => { + clearTimeout(timer) + child.stderr.off('data', onData) + child.off('exit', onExit) + child.off('error', onError) + } + + child.stderr.on('data', onData) + child.once('exit', onExit) + child.once('error', onError) + }) +} + +function serializeError(error) { + if (error instanceof Error) { + return { name: error.name, message: error.message, stack: error.stack } + } + return { message: String(error) } +} + +function toNdjson(values) { + return values.map((value) => JSON.stringify(value)).join('\n') + (values.length ? '\n' : '') +} + +export class ChromiumDevToolsFrontend { + constructor(inspectorUrl, { backend = 'native' } = {}) { + if (!Object.hasOwn(EXPECTED_PROTOCOL_ERRORS, backend)) { + throw new Error(`Unknown DevTools frontend backend: ${backend}`) + } + this.inspectorUrl = inspectorUrl + this.backend = backend + this.consoleMessages = [] + this.pageErrors = [] + this.crashes = [] + this.externalConnections = [] + this.browserOutput = { stdout: '', stderr: '' } + this.traceStarted = false + this.traceStopped = false + this.closed = false + } + + async start() { + this.userDataDirectory = await mkdtemp(join(tmpdir(), 'node-network-devtools-chromium-')) + const browserProcess = spawn( + chromium.executablePath(), + [ + '--headless=new', + '--remote-debugging-port=0', + `--user-data-dir=${this.userDataDirectory}`, + '--no-first-run', + '--no-default-browser-check', + '--no-sandbox', + '--no-proxy-server', + '--disable-background-networking', + '--disable-component-update', + '--disable-default-apps', + '--disable-domain-reliability', + '--disable-extensions', + '--disable-sync', + '--metrics-recording-only', + '--mute-audio', + '--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1' + ], + { stdio: ['ignore', 'pipe', 'pipe'] } + ) + this.browserProcess = browserProcess + browserProcess.stdout.setEncoding('utf8') + browserProcess.stderr.setEncoding('utf8') + browserProcess.stdout.on('data', (chunk) => { + this.browserOutput.stdout += chunk + }) + browserProcess.stderr.on('data', (chunk) => { + this.browserOutput.stderr += chunk + }) + + this.browserWebSocketUrl = await waitForBrowserWebSocket(browserProcess, this.browserOutput) + const browser = await chromium.connectOverCDP(this.browserWebSocketUrl) + this.browser = browser + this.browserVersion = browser.version() + + const [context] = browser.contexts() + if (!context) throw new Error('Chromium did not expose its default browser context') + this.context = context + await context.tracing.start({ screenshots: true, snapshots: true, sources: false }) + this.traceStarted = true + + const inspectorSocket = this.inspectorUrl.replace(/^ws:\/\//, '') + const browserEndpoint = new URL(this.browserWebSocketUrl) + // Chromium's local remote-debugging server exposes the exact official + // frontend bundled in this binary. /devtools/ is complete; /bundled/ is + // not a static asset root on this HTTP server. + this.frontendUrl = + `http://${browserEndpoint.host}/devtools/js_app.html?experiments=true&v8only=true` + + `&ws=${inspectorSocket}&hl=en-US` + + const page = await context.newPage() + this.page = page + page.on('console', (message) => { + this.consoleMessages.push({ + type: message.type(), + text: message.text(), + location: message.location() + }) + }) + page.on('pageerror', (error) => this.pageErrors.push(serializeError(error))) + page.on('crash', () => this.crashes.push({ timestamp: new Date().toISOString() })) + const recordExternalConnection = (kind, url) => { + try { + const parsed = new URL(url) + if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return + if (!['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)) { + this.externalConnections.push({ kind, url }) + } + } catch { + this.externalConnections.push({ kind, url }) + } + } + page.on('request', (request) => recordExternalConnection('request', request.url())) + page.on('websocket', (socket) => recordExternalConnection('websocket', socket.url())) + + await page.goto(this.frontendUrl, { + waitUntil: 'domcontentloaded', + timeout: DEFAULT_TIMEOUT_MS + }) + await this.openNetworkPanel() + return this + } + + async openNetworkPanel() { + const page = this.page + const networkTab = page.getByText('Network', { exact: true }).first() + await networkTab.waitFor({ state: 'visible', timeout: DEFAULT_TIMEOUT_MS }) + await networkTab.click() + await page.getByText('Fetch/XHR', { exact: true }).waitFor({ + state: 'visible', + timeout: DEFAULT_TIMEOUT_MS + }) + } + + async reload() { + await this.page.reload({ + waitUntil: 'domcontentloaded', + timeout: DEFAULT_TIMEOUT_MS + }) + await this.openNetworkPanel() + } + + async targetState() { + return this.page.evaluate(async () => { + const SDK = await import('./core/sdk/sdk.js') + const targetManager = SDK.TargetManager.TargetManager.instance() + const target = targetManager.primaryPageTarget() + return { + connected: Boolean(target), + targetCount: targetManager.targets().length, + type: target?.type(), + suspended: target?.suspended() + } + }) + } + + async requestState(url) { + return this.page.evaluate(async (requestUrl) => { + const Logs = await import('./models/logs/logs.js') + const request = Logs.NetworkLog.NetworkLog.instance() + .requests() + .find((candidate) => candidate.url() === requestUrl) + if (!request) return { found: false } + return { + found: true, + finished: Boolean(request.finished), + method: request.requestMethod, + mimeType: request.mimeType, + statusCode: request.statusCode, + url: request.url() + } + }, url) + } + + async requestBody(url) { + return this.page.evaluate(async (requestUrl) => { + const Logs = await import('./models/logs/logs.js') + const request = Logs.NetworkLog.NetworkLog.instance() + .requests() + .find((candidate) => candidate.url() === requestUrl) + if (!request) throw new Error(`Network model did not contain ${requestUrl}`) + if (!request.finished) throw new Error(`Network request was not finished: ${requestUrl}`) + const content = await request.requestContentData() + return { + text: content.text, + mimeType: content.mimeType, + charset: content.charset, + error: content.error ? String(content.error) : undefined + } + }, url) + } + + unexpectedErrors() { + const browserEndpoint = this.browserWebSocketUrl ? new URL(this.browserWebSocketUrl) : undefined + const missingFaviconUrl = browserEndpoint + ? `http://${browserEndpoint.host}/favicon.ico` + : undefined + const unexpectedConsole = this.consoleMessages.filter( + (message) => + message.type === 'error' && + !EXPECTED_PROTOCOL_ERRORS[this.backend].some((pattern) => pattern.test(message.text)) && + !( + message.location.url === missingFaviconUrl && + /^Failed to load resource: .*status of 404/.test(message.text) + ) + ) + return { + console: unexpectedConsole, + page: this.pageErrors, + crashes: this.crashes, + externalConnections: this.externalConnections + } + } + + assertNoUnexpectedErrors() { + const errors = this.unexpectedErrors() + if ( + errors.console.length || + errors.page.length || + errors.crashes.length || + errors.externalConnections.length + ) { + throw new Error(`Unexpected DevTools frontend errors:\n${JSON.stringify(errors, null, 2)}`) + } + } + + async captureEvidence(testInfo, prefix = `${this.backend}-devtools`) { + await mkdir(testInfo.outputDir, { recursive: true }) + const attachments = [] + const attachFile = async (name, content, contentType = 'text/plain') => { + const path = testInfo.outputPath(name) + await writeFile(path, content) + await testInfo.attach(name, { path, contentType }) + attachments.push(path) + } + + if (this.page && !this.page.isClosed()) { + const screenshotName = `${prefix}-frontend.png` + const screenshotPath = testInfo.outputPath(screenshotName) + await this.page.screenshot({ path: screenshotPath, fullPage: true }) + await testInfo.attach(screenshotName, { + path: screenshotPath, + contentType: 'image/png' + }) + attachments.push(screenshotPath) + } + + if (this.traceStarted && !this.traceStopped) { + const traceName = `${prefix}-trace.zip` + const tracePath = testInfo.outputPath(traceName) + await this.context.tracing.stop({ path: tracePath }) + this.traceStopped = true + await testInfo.attach(traceName, { path: tracePath, contentType: 'application/zip' }) + attachments.push(tracePath) + } + + await Promise.all([ + attachFile(`${prefix}-console.ndjson`, toNdjson(this.consoleMessages)), + attachFile(`${prefix}-page-errors.ndjson`, toNdjson(this.pageErrors)), + attachFile(`${prefix}-chromium.stdout.log`, this.browserOutput.stdout), + attachFile(`${prefix}-chromium.stderr.log`, this.browserOutput.stderr), + attachFile( + `${prefix}-metadata.json`, + JSON.stringify( + { + backend: this.backend, + playwrightChromium: this.browserVersion, + chromiumExecutable: chromium.executablePath(), + frontendUrl: this.frontendUrl, + inspectorUrl: this.inspectorUrl, + actualPageUrls: this.context?.pages().map((page) => page.url()) ?? [], + unexpectedErrors: this.unexpectedErrors() + }, + null, + 2 + ), + 'application/json' + ) + ]) + + return attachments + } + + async captureFailure(testInfo, error, targetHarness) { + const attachments = [] + const attachFile = async (name, content, contentType = 'text/plain') => { + const path = testInfo.outputPath(name) + await mkdir(testInfo.outputDir, { recursive: true }) + await writeFile(path, content) + await testInfo.attach(name, { path, contentType }) + attachments.push(path) + } + + if (this.page && !this.page.isClosed()) { + const screenshotPath = testInfo.outputPath('devtools-frontend.png') + try { + await this.page.screenshot({ path: screenshotPath, fullPage: true }) + await testInfo.attach('devtools-frontend.png', { + path: screenshotPath, + contentType: 'image/png' + }) + attachments.push(screenshotPath) + } catch (screenshotError) { + await attachFile( + 'devtools-screenshot-error.json', + JSON.stringify(serializeError(screenshotError), null, 2), + 'application/json' + ) + } + } + + if (this.traceStarted && !this.traceStopped) { + const tracePath = testInfo.outputPath('devtools-trace.zip') + await this.context.tracing.stop({ path: tracePath }).catch(() => undefined) + this.traceStopped = true + await testInfo + .attach('devtools-trace.zip', { path: tracePath, contentType: 'application/zip' }) + .catch(() => {}) + } + + await Promise.all([ + attachFile('frontend-console.ndjson', toNdjson(this.consoleMessages)), + attachFile('frontend-page-errors.ndjson', toNdjson(this.pageErrors)), + attachFile('chromium.stdout.log', this.browserOutput.stdout), + attachFile('chromium.stderr.log', this.browserOutput.stderr), + attachFile( + 'frontend-metadata.json', + JSON.stringify( + { + error: serializeError(error), + backend: this.backend, + playwrightChromium: this.browserVersion, + chromiumExecutable: chromium.executablePath(), + frontendUrl: this.frontendUrl, + inspectorUrl: this.inspectorUrl, + actualPageUrls: this.context?.pages().map((page) => page.url()) ?? [], + unexpectedErrors: this.unexpectedErrors() + }, + null, + 2 + ), + 'application/json' + ), + attachFile(`${this.backend}-target.stdout.log`, targetHarness.stdout), + attachFile(`${this.backend}-target.stderr.log`, targetHarness.stderr), + attachFile(`${this.backend}-target-messages.ndjson`, toNdjson(targetHarness.targetMessages)) + ]) + + if (targetHarness.client) { + const journalPath = testInfo.outputPath(`${this.backend}-cdp-${randomUUID()}.ndjson`) + await targetHarness.client.writeJournal(journalPath) + await testInfo.attach(`${this.backend}-cdp.ndjson`, { + path: journalPath, + contentType: 'application/x-ndjson' + }) + attachments.push(journalPath) + } + return attachments + } + + async close() { + if (this.closed) return + this.closed = true + const errors = [] + + if (this.traceStarted && !this.traceStopped) { + try { + await this.context.tracing.stop() + this.traceStopped = true + } catch (error) { + errors.push(error) + } + } + + try { + await this.browser?.close() + } catch (error) { + errors.push(error) + } + + if ( + this.browserProcess && + this.browserProcess.exitCode === null && + this.browserProcess.signalCode === null + ) { + this.browserProcess.kill('SIGTERM') + try { + await waitForExit(this.browserProcess) + } catch { + this.browserProcess.kill('SIGKILL') + await waitForExit(this.browserProcess, 2_000).catch((error) => errors.push(error)) + } + } + + if (this.userDataDirectory) { + try { + await rm(this.userDataDirectory, { + recursive: true, + force: true, + maxRetries: 5, + retryDelay: 100 + }) + } catch (error) { + errors.push(error) + } + } + + if (errors.length) throw new AggregateError(errors, 'Failed to clean up Chromium frontend') + } +} diff --git a/packages/network-debugger/test/e2e/frontend/legacy-frontend.spec.mjs b/packages/network-debugger/test/e2e/frontend/legacy-frontend.spec.mjs new file mode 100644 index 0000000..b6fc485 --- /dev/null +++ b/packages/network-debugger/test/e2e/frontend/legacy-frontend.spec.mjs @@ -0,0 +1,153 @@ +import { mkdir } from 'node:fs/promises' + +import { expect, test } from '@playwright/test' + +import { CdpClient } from '../harness/cdp-client.mjs' +import { LegacyE2EHarness } from '../legacy/legacy-harness.mjs' +import { ChromiumDevToolsFrontend } from './chromium-devtools.mjs' + +async function expectConnected(frontend) { + await expect + .poll(() => frontend.targetState(), { + message: 'the official frontend should have one live Legacy Node target' + }) + .toMatchObject({ + connected: true, + targetCount: 1, + type: 'node', + suspended: false + }) +} + +function eventForUrl(client, method, url) { + return client.waitForEvent(method, (params) => { + const eventUrl = params.request?.url ?? params.response?.url ?? params.url + return eventUrl === url + }) +} + +function decodeBody(result) { + return result.base64Encoded ? Buffer.from(result.body, 'base64').toString('utf8') : result.body +} + +async function attachJournal(testInfo, name, client) { + await mkdir(testInfo.outputDir, { recursive: true }) + const path = testInfo.outputPath(name) + await client.writeJournal(path) + await testInfo.attach(name, { path, contentType: 'application/x-ndjson' }) +} + +async function exerciseFetch(frontend, legacyHarness, secondClient, label) { + const token = legacyHarness.createToken(label) + const url = legacyHarness.url('/post', token) + const requestPromise = eventForUrl(legacyHarness.client, 'Network.requestWillBeSent', url) + const scenarioPromise = legacyHarness.runScenario('fetch-post', { url, token }) + const request = await requestPromise + const scenario = await scenarioPromise + + await expect + .poll(() => frontend.requestState(url), { + message: 'the official DevTools Network model should finish the Legacy request' + }) + .toMatchObject({ + found: true, + finished: true, + method: 'POST', + mimeType: 'application/json', + statusCode: 201, + url + }) + + await expect(frontend.page.getByText(token, { exact: false }).first()).toBeVisible() + + // Both raw clients started at command id 1. Issue different commands with + // the same next id while the official frontend is also requesting content. + // A broadcast or incorrectly routed response makes one of these assertions + // fail immediately. + const frontendBodyPromise = frontend.requestBody(url) + const rawBodyPromise = legacyHarness.client.command('Network.getResponseBody', { + requestId: request.message.params.requestId + }) + const secondClientErrorPromise = secondClient + .command('NodeNetworkDevtools.frontendMultiClientProbe') + .then( + () => undefined, + (error) => error + ) + const [content, rawBody, secondClientError] = await Promise.all([ + frontendBodyPromise, + rawBodyPromise, + secondClientErrorPromise + ]) + + expect(secondClientError?.cdpError?.code).toBe(-32601) + expect(secondClientError.cdpResponse.id).toBe(rawBody.request.id) + expect(rawBody.response.id).toBe(rawBody.request.id) + expect(content.error).toBeUndefined() + expect(content.mimeType).toBe('application/json') + expect(content.charset).toBe('utf-8') + + const expected = { + token, + requestBody: `fetch-request:${token}` + } + expect(JSON.parse(content.text)).toEqual(expected) + expect(JSON.parse(decodeBody(rawBody.result))).toEqual(expected) + expect(JSON.parse(scenario.body)).toEqual(expected) +} + +test('official Chromium DevTools frontend displays Legacy requests across reload and isolates clients', async ({}, testInfo) => { + const legacyHarness = new LegacyE2EHarness({ consumer: 'esm' }) + let frontend + let secondClient + let failure + + try { + await legacyHarness.start() + secondClient = new CdpClient(legacyHarness.sessionInfo.target.webSocketDebuggerUrl) + await secondClient.connect() + const secondEnable = await secondClient.command('Network.enable') + expect(secondEnable.response.id).toBe(secondEnable.request.id) + + frontend = new ChromiumDevToolsFrontend(legacyHarness.sessionInfo.target.webSocketDebuggerUrl, { + backend: 'legacy' + }) + await frontend.start() + await expect(frontend.page).toHaveTitle(/DevTools/) + const inspectorSocket = legacyHarness.sessionInfo.target.webSocketDebuggerUrl.replace( + /^ws:\/\//, + '' + ) + expect(new URL(frontend.page.url()).searchParams.get('ws')).toBe(inspectorSocket) + await expectConnected(frontend) + + await exerciseFetch(frontend, legacyHarness, secondClient, 'legacy-frontend-initial') + + await frontend.reload() + await expect(frontend.page).toHaveTitle(/DevTools/) + expect(new URL(frontend.page.url()).searchParams.get('ws')).toBe(inspectorSocket) + await expectConnected(frontend) + await exerciseFetch(frontend, legacyHarness, secondClient, 'legacy-frontend-reconnected') + + frontend.assertNoUnexpectedErrors() + await frontend.captureEvidence(testInfo, 'legacy-devtools') + await attachJournal(testInfo, 'legacy-primary-cdp.ndjson', legacyHarness.client) + await attachJournal(testInfo, 'legacy-secondary-cdp.ndjson', secondClient) + } catch (error) { + failure = error + await frontend?.captureFailure(testInfo, error, legacyHarness).catch(() => {}) + if (secondClient) { + await attachJournal(testInfo, 'legacy-secondary-cdp.ndjson', secondClient).catch(() => {}) + } + await legacyHarness.writeFailureArtifacts(error).catch(() => undefined) + throw error + } finally { + const cleanupErrors = [] + await frontend?.close().catch((error) => cleanupErrors.push(error)) + await secondClient?.close().catch((error) => cleanupErrors.push(error)) + await legacyHarness.close().catch((error) => cleanupErrors.push(error)) + if (!failure && cleanupErrors.length) { + throw new AggregateError(cleanupErrors, 'Legacy frontend E2E cleanup failed') + } + } +}) diff --git a/packages/network-debugger/test/e2e/frontend/native-frontend.spec.mjs b/packages/network-debugger/test/e2e/frontend/native-frontend.spec.mjs new file mode 100644 index 0000000..6befb14 --- /dev/null +++ b/packages/network-debugger/test/e2e/frontend/native-frontend.spec.mjs @@ -0,0 +1,86 @@ +import { expect, test } from '@playwright/test' + +import { NativeE2EHarness } from '../harness/native-harness.mjs' +import { ChromiumDevToolsFrontend } from './chromium-devtools.mjs' + +async function expectConnected(frontend) { + await expect + .poll(() => frontend.targetState(), { + message: 'the official frontend should have a live Node target' + }) + .toMatchObject({ + connected: true, + targetCount: 1, + type: 'node', + suspended: false + }) +} + +async function exerciseFetch(frontend, nativeHarness, label) { + const token = nativeHarness.createToken(label) + const url = nativeHarness.url('/fetch', token) + const scenario = await nativeHarness.runScenario('fetch', { url, token }) + + await expect + .poll(() => frontend.requestState(url), { + message: 'the official DevTools Network model should finish the Native request' + }) + .toMatchObject({ + found: true, + finished: true, + method: 'POST', + mimeType: 'application/json', + statusCode: 201, + url + }) + + await expect(frontend.page.getByText(token, { exact: false }).first()).toBeVisible() + + const content = await frontend.requestBody(url) + expect(content.error).toBeUndefined() + expect(content.mimeType).toBe('application/json') + expect(content.charset).toBe('utf-8') + expect(JSON.parse(content.text)).toEqual({ + token, + requestBody: `fetch-request:${token}` + }) + expect(JSON.parse(scenario.body)).toEqual(JSON.parse(content.text)) +} + +test('official Chromium DevTools frontend displays Native requests before and after reconnecting', async ({}, testInfo) => { + const nativeHarness = new NativeE2EHarness() + let frontend + let failure + + try { + await nativeHarness.start() + // Construct before starting so launch failures still retain Chromium logs, + // screenshots/traces where available, and the Native CDP journal. + frontend = new ChromiumDevToolsFrontend(nativeHarness.inspectorUrl) + await frontend.start() + await expect(frontend.page).toHaveTitle(/DevTools/) + const inspectorSocket = nativeHarness.inspectorUrl.replace(/^ws:\/\//, '') + expect(new URL(frontend.page.url()).searchParams.get('ws')).toBe(inspectorSocket) + await expectConnected(frontend) + + await exerciseFetch(frontend, nativeHarness, 'frontend-initial') + + await frontend.reload() + await expect(frontend.page).toHaveTitle(/DevTools/) + expect(new URL(frontend.page.url()).searchParams.get('ws')).toBe(inspectorSocket) + await expectConnected(frontend) + await exerciseFetch(frontend, nativeHarness, 'frontend-reconnected') + frontend.assertNoUnexpectedErrors() + } catch (error) { + failure = error + await frontend?.captureFailure(testInfo, error, nativeHarness).catch(() => {}) + throw error + } finally { + const cleanupErrors = [] + await frontend?.close().catch((error) => cleanupErrors.push(error)) + await nativeHarness.close().catch((error) => cleanupErrors.push(error)) + if (!failure && cleanupErrors.length) { + throw new AggregateError(cleanupErrors, 'Frontend E2E cleanup failed') + } + } +}) diff --git a/packages/network-debugger/test/e2e/frontend/playwright.config.mjs b/packages/network-debugger/test/e2e/frontend/playwright.config.mjs new file mode 100644 index 0000000..f1f8023 --- /dev/null +++ b/packages/network-debugger/test/e2e/frontend/playwright.config.mjs @@ -0,0 +1,20 @@ +import { tmpdir } from 'node:os' +import { resolve } from 'node:path' + +import { defineConfig } from '@playwright/test' + +const outputDirectory = process.env.NETWORK_DEBUGGER_E2E_ARTIFACT_DIR + ? resolve(process.env.NETWORK_DEBUGGER_E2E_ARTIFACT_DIR, 'frontend') + : resolve(tmpdir(), `node-network-devtools-playwright-${process.pid}`) + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.mjs', + timeout: 45_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + workers: 1, + retries: 0, + outputDir: outputDirectory, + reporter: process.env.CI ? [['line']] : [['list']] +}) diff --git a/packages/network-debugger/test/e2e/harness/cdp-client.mjs b/packages/network-debugger/test/e2e/harness/cdp-client.mjs new file mode 100644 index 0000000..e668932 --- /dev/null +++ b/packages/network-debugger/test/e2e/harness/cdp-client.mjs @@ -0,0 +1,262 @@ +import { writeFile } from 'node:fs/promises' + +import WebSocket from 'ws' + +const DEFAULT_TIMEOUT_MS = 10_000 + +function timeoutError(label, timeoutMs) { + return new Error(`Timed out after ${timeoutMs}ms while waiting for ${label}`) +} + +function stringifyJournalEntry(entry) { + return JSON.stringify(entry, (_key, value) => + typeof value === 'bigint' ? value.toString() : value + ) +} + +/** + * A deliberately small CDP client. It speaks JSON-RPC over a real WebSocket and + * does not know anything about the network debugger implementation. + */ +export class CdpClient { + #socket + #nextCommandId = 0 + #nextSequence = 0 + #pendingCommands = new Map() + #eventWaiters = new Set() + #closed = false + + constructor(webSocketUrl) { + this.webSocketUrl = webSocketUrl + this.journal = [] + this.events = [] + } + + async connect({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + if (this.#socket) { + throw new Error('CDP client is already connected') + } + + const socket = new WebSocket(this.webSocketUrl) + this.#socket = socket + + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + socket.terminate() + reject(timeoutError(`CDP WebSocket ${this.webSocketUrl}`, timeoutMs)) + }, timeoutMs) + + const cleanup = () => { + clearTimeout(timer) + socket.off('open', onOpen) + socket.off('error', onInitialError) + socket.off('unexpected-response', onUnexpectedResponse) + } + + const onOpen = () => { + cleanup() + resolve() + } + + const onInitialError = (error) => { + cleanup() + reject(error) + } + + const onUnexpectedResponse = (_request, response) => { + cleanup() + reject( + new Error( + `Unexpected HTTP ${response.statusCode} while connecting to ${this.webSocketUrl}` + ) + ) + } + + socket.once('open', onOpen) + socket.once('error', onInitialError) + socket.once('unexpected-response', onUnexpectedResponse) + }) + + socket.on('message', (data) => this.#onMessage(data)) + socket.on('error', (error) => this.#onDisconnect(error)) + socket.on('close', (code, reason) => { + this.#onDisconnect( + new Error(`CDP WebSocket closed (${code}): ${reason.toString() || 'no reason'}`) + ) + }) + + this.#record('connection', { state: 'open', url: this.webSocketUrl }) + return this + } + + async command(method, params = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const socket = this.#socket + if (!socket || socket.readyState !== WebSocket.OPEN) { + throw new Error(`Cannot send ${method}: CDP WebSocket is not open`) + } + + const id = ++this.#nextCommandId + const request = { id, method, params } + this.#record('send', request) + + const response = await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.#pendingCommands.delete(id) + reject(timeoutError(`response to CDP command ${method} (${id})`, timeoutMs)) + }, timeoutMs) + + this.#pendingCommands.set(id, { + method, + resolve: (message) => { + clearTimeout(timer) + resolve(message) + }, + reject: (error) => { + clearTimeout(timer) + reject(error) + } + }) + + socket.send(JSON.stringify(request), (error) => { + if (!error) return + const pending = this.#pendingCommands.get(id) + if (!pending) return + this.#pendingCommands.delete(id) + pending.reject(error) + }) + }) + + if (response.error) { + const error = new Error( + `CDP command ${method} failed (${response.error.code}): ${response.error.message}` + ) + error.cdpError = response.error + error.cdpResponse = response + throw error + } + + return { + id, + request, + response, + result: response.result + } + } + + waitForEvent(method, predicate = () => true, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const existing = this.events.find( + (record) => + record.message.method === method && predicate(record.message.params, record.message) + ) + if (existing) return Promise.resolve(existing) + + return new Promise((resolve, reject) => { + const waiter = { method, predicate, resolve, reject, timer: undefined } + waiter.timer = setTimeout(() => { + this.#eventWaiters.delete(waiter) + const observed = this.events.map((record) => record.message.method).join(', ') + reject( + new Error( + `${timeoutError(`CDP event ${method}`, timeoutMs).message}. Observed: ${observed || 'none'}` + ) + ) + }, timeoutMs) + this.#eventWaiters.add(waiter) + }) + } + + findEvents(method, predicate = () => true) { + return this.events.filter( + (record) => + record.message.method === method && predicate(record.message.params, record.message) + ) + } + + async writeJournal(path) { + const body = this.journal.map(stringifyJournalEntry).join('\n') + await writeFile(path, body ? `${body}\n` : '', 'utf8') + } + + async close({ timeoutMs = 2_000 } = {}) { + const socket = this.#socket + if (!socket || socket.readyState === WebSocket.CLOSED) return + + this.#closed = true + await new Promise((resolve) => { + const timer = setTimeout(() => { + socket.terminate() + resolve() + }, timeoutMs) + + socket.once('close', () => { + clearTimeout(timer) + resolve() + }) + socket.close() + }) + } + + #record(direction, message) { + const record = { + sequence: ++this.#nextSequence, + timestamp: new Date().toISOString(), + direction, + message + } + this.journal.push(record) + return record + } + + #onMessage(data) { + let message + try { + message = JSON.parse(data.toString()) + } catch (error) { + this.#record('receive-invalid', { raw: data.toString(), error: String(error) }) + this.#onDisconnect(error) + return + } + + const record = this.#record('receive', message) + if (message.id !== undefined) { + const pending = this.#pendingCommands.get(message.id) + if (!pending) return + this.#pendingCommands.delete(message.id) + pending.resolve(message) + return + } + + if (!message.method) return + this.events.push(record) + + for (const waiter of [...this.#eventWaiters]) { + if (waiter.method !== message.method) continue + try { + if (!waiter.predicate(message.params, message)) continue + } catch (error) { + clearTimeout(waiter.timer) + this.#eventWaiters.delete(waiter) + waiter.reject(error) + continue + } + clearTimeout(waiter.timer) + this.#eventWaiters.delete(waiter) + waiter.resolve(record) + } + } + + #onDisconnect(error) { + if (this.#closed) return + this.#record('connection', { state: 'closed', error: String(error) }) + for (const [id, pending] of this.#pendingCommands) { + pending.reject(new Error(`CDP connection ended before ${pending.method} (${id}): ${error}`)) + } + this.#pendingCommands.clear() + for (const waiter of this.#eventWaiters) { + clearTimeout(waiter.timer) + waiter.reject(new Error(`CDP connection ended while waiting for ${waiter.method}: ${error}`)) + } + this.#eventWaiters.clear() + } +} diff --git a/packages/network-debugger/test/e2e/harness/native-harness.mjs b/packages/network-debugger/test/e2e/harness/native-harness.mjs new file mode 100644 index 0000000..c76db80 --- /dev/null +++ b/packages/network-debugger/test/e2e/harness/native-harness.mjs @@ -0,0 +1,461 @@ +import { spawn } from 'node:child_process' +import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { randomUUID } from 'node:crypto' + +import { CdpClient } from './cdp-client.mjs' +import { startOriginServer } from '../fixtures/origin-server.mjs' + +const HARNESS_DIR = dirname(fileURLToPath(import.meta.url)) +const PACKAGE_DIR = resolve(HARNESS_DIR, '../../..') +const DEFAULT_ENTRY_PATH = resolve(PACKAGE_DIR, 'dist/index.mjs') +const TARGET_PATH = resolve(HARNESS_DIR, '../fixtures/native-target.mjs') +const DEFAULT_TIMEOUT_MS = 10_000 + +function waitForEmitter({ + emitter, + event, + predicate, + timeoutMs, + label, + history, + endEvent, + endError +}) { + const existing = history?.find(predicate) + if (existing) return Promise.resolve(existing) + + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Timed out after ${timeoutMs}ms while waiting for ${label}`)) + }, timeoutMs) + + const onValue = (value) => { + let matches + try { + matches = predicate(value) + } catch (error) { + cleanup() + reject(error) + return + } + if (!matches) return + cleanup() + resolvePromise(value) + } + + const onError = (error) => { + cleanup() + reject(error) + } + + const onEnd = (...args) => { + cleanup() + reject(endError(...args)) + } + + const cleanup = () => { + clearTimeout(timer) + emitter.off(event, onValue) + emitter.off('error', onError) + if (endEvent) emitter.off(endEvent, onEnd) + } + + emitter.on(event, onValue) + emitter.once('error', onError) + if (endEvent) emitter.once(endEvent, onEnd) + }) +} + +function waitForExit(child, timeoutMs, label) { + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.resolve({ code: child.exitCode, signal: child.signalCode }) + } + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Timed out after ${timeoutMs}ms while waiting for ${label}`)) + }, timeoutMs) + const onExit = (code, signal) => { + cleanup() + resolvePromise({ code, signal }) + } + const cleanup = () => { + clearTimeout(timer) + child.off('exit', onExit) + } + child.once('exit', onExit) + }) +} + +function serializeError(error) { + if (error instanceof Error) { + return { name: error.name, message: error.message, stack: error.stack } + } + return { message: String(error) } +} + +export class NativeE2EHarness { + constructor({ + entryPath = process.env.NETWORK_DEBUGGER_E2E_ENTRY ?? DEFAULT_ENTRY_PATH, + recordSession = false + } = {}) { + this.entryPath = entryPath + this.recordSession = recordSession + this.targetMessages = [] + this.stdout = '' + this.stderr = '' + this.closed = false + this.keepSessionArtifacts = false + } + + async start() { + await access(this.entryPath).catch(() => { + throw new Error( + `Built public package entry is missing at ${this.entryPath}. ` + + 'Run `pnpm --filter node-network-devtools build` before the E2E suite.' + ) + }) + + if (this.recordSession) { + this.temporaryDirectory = await mkdtemp( + join(tmpdir(), 'node-network-devtools-native-session-e2e-') + ) + this.sessionDirectory = join(this.temporaryDirectory, 'session') + } + + this.origin = await startOriginServer() + const child = spawn( + process.execPath, + ['--inspect-wait=127.0.0.1:0', '--experimental-network-inspection', TARGET_PATH], + { + cwd: PACKAGE_DIR, + env: { + ...process.env, + NETWORK_DEBUGGER_E2E_ENTRY: pathToFileURL(this.entryPath).href, + ...(this.sessionDirectory + ? { NETWORK_DEBUGGER_E2E_SESSION_DIR: this.sessionDirectory } + : {}) + }, + stdio: ['ignore', 'pipe', 'pipe', 'ipc'] + } + ) + this.child = child + + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + this.stdout += chunk + }) + child.stderr.on('data', (chunk) => { + this.stderr += chunk + }) + child.on('message', (message) => this.targetMessages.push(message)) + + const inspectorUrl = await this.#waitForInspectorUrl() + this.inspectorUrl = inspectorUrl + const client = new CdpClient(inspectorUrl) + this.client = client + await client.connect() + + this.networkEnable = await client.command('Network.enable') + this.runIfWaiting = await client.command('Runtime.runIfWaitingForDebugger') + + const ready = await this.waitForTargetMessage((message) => message?.type === 'ready') + this.targetPid = ready.pid + this.sessionInfo = ready.sessionInfo + this.nativeFunctionsUnchanged = ready.nativeFunctionsUnchanged + return this + } + + createToken(prefix) { + return `${prefix}-${randomUUID()}` + } + + url(pathname, token) { + return `${this.origin.baseUrl}${pathname}?token=${encodeURIComponent(token)}` + } + + async runScenario(scenario, { url, token, timeoutMs = DEFAULT_TIMEOUT_MS }) { + const id = randomUUID() + const resultPromise = this.waitForTargetMessage( + (message) => message?.type === 'scenario-result' && message.id === id, + { timeoutMs } + ) + + await new Promise((resolvePromise, reject) => { + this.child.send({ type: 'run', id, scenario, url, token }, (error) => + error ? reject(error) : resolvePromise() + ) + }) + + const message = await resultPromise + if (!message.ok) throw new Error(`Target scenario ${scenario} failed:\n${message.error}`) + return message.result + } + + async finalizeSession({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + if (!this.sessionDirectory) throw new Error('Native Session recording is not enabled') + const id = randomUUID() + const resultPromise = this.waitForTargetMessage( + (message) => message?.type === 'session-finalized' && message.id === id, + { timeoutMs } + ) + await new Promise((resolvePromise, reject) => { + this.child.send({ type: 'finalize-session', id }, (error) => + error ? reject(error) : resolvePromise() + ) + }) + const message = await resultPromise + if (!message.ok) throw new Error(`Native Session finalization failed:\n${message.error}`) + return message.result + } + + async waitForRecordedRequest(url, { requireBody = true, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + if (!this.sessionDirectory) throw new Error('Native Session recording is not enabled') + + const manifestPath = resolve(this.sessionDirectory, 'manifest.json') + const deadline = Date.now() + timeoutMs + let lastManifest + let lastReadError + + while (Date.now() < deadline) { + try { + lastManifest = JSON.parse(await readFile(manifestPath, 'utf8')) + lastReadError = undefined + const request = Object.values(lastManifest.requestIndex ?? {}).find( + (entry) => entry?.request?.url === url + ) + const body = request?.requestId ? lastManifest.bodyIndex?.[request.requestId] : undefined + + if ( + request?.response && + Number.isFinite(request.finishedTimestamp) && + (!requireBody || body) + ) { + return { manifest: lastManifest, request, body } + } + } catch (error) { + lastReadError = error + } + + await new Promise((resolvePromise) => setTimeout(resolvePromise, 25)) + } + + const observed = lastManifest + ? JSON.stringify({ stats: lastManifest.stats, issues: lastManifest.issues }) + : lastReadError instanceof Error + ? lastReadError.message + : 'no readable manifest' + throw new Error( + `Timed out after ${timeoutMs}ms waiting for Session to record ${url}. Observed: ${observed}` + ) + } + + waitForTargetMessage(predicate, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + if (this.child.exitCode !== null || this.child.signalCode !== null) { + return Promise.reject( + new Error( + `Native target already exited (code=${this.child.exitCode}, signal=${this.child.signalCode}).\n` + + this.stderr + ) + ) + } + return waitForEmitter({ + emitter: this.child, + event: 'message', + predicate, + timeoutMs, + label: 'target IPC message', + history: this.targetMessages, + endEvent: 'exit', + endError: (code, signal) => + new Error( + `Native target exited while waiting for IPC (code=${code}, signal=${signal}).\n` + + this.stderr + ) + }) + } + + async writeFailureArtifacts(error) { + const configuredRoot = process.env.NETWORK_DEBUGGER_E2E_ARTIFACT_DIR + let artifactDirectory + if (configuredRoot) { + await mkdir(configuredRoot, { recursive: true }) + artifactDirectory = join( + configuredRoot, + `native-${Date.now()}-${process.pid}-${randomUUID()}` + ) + await mkdir(artifactDirectory, { recursive: true }) + } else { + artifactDirectory = await mkdtemp(join(tmpdir(), 'node-network-devtools-native-e2e-')) + } + + await Promise.all([ + this.client?.writeJournal(join(artifactDirectory, 'cdp.ndjson')) ?? Promise.resolve(), + writeFile(join(artifactDirectory, 'target.stdout.log'), this.stdout, 'utf8'), + writeFile(join(artifactDirectory, 'target.stderr.log'), this.stderr, 'utf8'), + writeFile( + join(artifactDirectory, 'target-messages.ndjson'), + this.targetMessages.map((message) => JSON.stringify(message)).join('\n') + '\n', + 'utf8' + ), + writeFile( + join(artifactDirectory, 'metadata.json'), + JSON.stringify( + { + error: serializeError(error), + node: process.version, + platform: process.platform, + inspectorUrl: this.inspectorUrl, + targetPid: this.targetPid, + sessionInfo: this.sessionInfo, + nativeFunctionsUnchanged: this.nativeFunctionsUnchanged + }, + null, + 2 + ), + 'utf8' + ) + ]) + + return artifactDirectory + } + + async close() { + if (this.closed) return + this.closed = true + const errors = [] + + if (this.child && this.child.exitCode === null && this.child.signalCode === null) { + try { + const shutdown = this.waitForTargetMessage( + (message) => message?.type === 'shutdown-complete', + { timeoutMs: 3_000 } + ) + await new Promise((resolvePromise, reject) => { + this.child.send({ type: 'shutdown' }, (error) => + error ? reject(error) : resolvePromise() + ) + }) + await shutdown + } catch (error) { + errors.push(error) + } + } + + try { + await this.client?.close() + } catch (error) { + errors.push(error) + } + + if (this.child && this.child.exitCode === null && this.child.signalCode === null) { + try { + await waitForExit(this.child, 3_000, 'native target to exit') + } catch (error) { + errors.push(error) + this.child.kill('SIGTERM') + try { + await waitForExit(this.child, 2_000, 'native target to terminate') + } catch (terminationError) { + errors.push(terminationError) + this.child.kill('SIGKILL') + } + } + } + + try { + await this.origin?.close() + } catch (error) { + errors.push(error) + } + + if (this.temporaryDirectory && !this.keepSessionArtifacts) { + try { + await rm(this.temporaryDirectory, { recursive: true, force: true }) + } catch (error) { + errors.push(error) + } + } + + if (errors.length) throw new AggregateError(errors, 'Failed to clean up native E2E harness') + } + + async #waitForInspectorUrl({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const existing = this.stderr.match(/Debugger listening on (ws:\/\/[^\s]+)/)?.[1] + if (existing) return existing + + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject( + new Error( + `Timed out after ${timeoutMs}ms waiting for Node Inspector URL. stderr:\n${this.stderr}` + ) + ) + }, timeoutMs) + + const onData = () => { + const match = this.stderr.match(/Debugger listening on (ws:\/\/[^\s]+)/) + if (!match) return + cleanup() + resolvePromise(match[1]) + } + + const onExit = (code, signal) => { + cleanup() + reject( + new Error( + `Native target exited before Inspector startup (code=${code}, signal=${signal}).\n` + + this.stderr + ) + ) + } + + const onError = (error) => { + cleanup() + reject(error) + } + + const cleanup = () => { + clearTimeout(timer) + this.child.stderr.off('data', onData) + this.child.off('exit', onExit) + this.child.off('error', onError) + } + + this.child.stderr.on('data', onData) + this.child.once('exit', onExit) + this.child.once('error', onError) + }) + } +} + +export async function withNativeHarness(testContext, callback, options = {}) { + const harness = new NativeE2EHarness(options) + testContext.after(async () => harness.close()) + + try { + await harness.start() + return await callback(harness) + } catch (error) { + harness.keepSessionArtifacts = true + const artifactDirectory = await harness.writeFailureArtifacts(error).catch(() => undefined) + if (artifactDirectory) { + console.error(`Native E2E failure artifacts: ${artifactDirectory}`) + if (error instanceof Error) { + error.message = `${error.message}\nFailure artifacts: ${artifactDirectory}` + if (harness.sessionDirectory) { + error.message += `\nNative Session artifacts: ${harness.sessionDirectory}` + } + } else { + error = new Error(`${String(error)}\nFailure artifacts: ${artifactDirectory}`) + } + } + throw error + } +} diff --git a/packages/network-debugger/test/e2e/legacy/README.md b/packages/network-debugger/test/e2e/legacy/README.md new file mode 100644 index 0000000..1f493e1 --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/README.md @@ -0,0 +1,20 @@ +# Real Legacy protocol E2E + +This suite starts the built ESM or CommonJS public package in a real child +process with `mode: "legacy"`. Test control uses the child's IPC channel. HTTP, +Fetch, SSE, and WebSocket traffic goes to a real loopback origin and is observed +only through a raw WebSocket CDP client. + +The contract intentionally requires the v2 Legacy architecture: + +- `legacy.serverPort: 0` binds the target on an OS-assigned port; +- `registration.ready.target` agrees with `/json/list`; +- `/json/version` and `/json/protocol` are served by the same endpoint; +- every CDP command id receives either `result` or a standard `error`; +- failed requests terminate with `Network.loadingFailed`, never a manufactured + successful lifecycle. + +No test imports Legacy plugins, mocks the bridge, or manufactures protocol +events. On failure the harness writes the raw CDP journal, origin journal, +consumer stdout/stderr, IPC records, and target discovery metadata to a temporary +artifact directory (or `NETWORK_DEBUGGER_E2E_ARTIFACT_DIR` when configured). diff --git a/packages/network-debugger/test/e2e/legacy/fixtures/consumer.cjs b/packages/network-debugger/test/e2e/legacy/fixtures/consumer.cjs new file mode 100644 index 0000000..86db8d3 --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/fixtures/consumer.cjs @@ -0,0 +1,31 @@ +'use strict' + +const { resolve } = require('node:path') + +const entryPath = process.env.NETWORK_DEBUGGER_E2E_ENTRY +if (!entryPath) throw new Error('NETWORK_DEBUGGER_E2E_ENTRY is required') + +const { register } = require(entryPath) +if (typeof register !== 'function') { + throw new Error(`No public register() export found at ${entryPath}`) +} + +const handle = register({ + mode: 'legacy', + devtools: { open: false }, + legacy: { serverPort: 0 } +}) +const readyPromise = handle.ready + +Promise.resolve(readyPromise) + .then(async (ready) => { + if (ready.mode !== 'legacy') { + throw new Error(`Expected forced Legacy adapter, received ${ready.mode}`) + } + const { installScenarioController } = require(resolve(__dirname, 'scenario-runner.cjs')) + await installScenarioController({ handle, ready, consumer: 'cjs' }) + }) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/packages/network-debugger/test/e2e/legacy/fixtures/consumer.mjs b/packages/network-debugger/test/e2e/legacy/fixtures/consumer.mjs new file mode 100644 index 0000000..13d0ae0 --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/fixtures/consumer.mjs @@ -0,0 +1,26 @@ +import { createRequire } from 'node:module' +import { dirname, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const entryPath = process.env.NETWORK_DEBUGGER_E2E_ENTRY +if (!entryPath) throw new Error('NETWORK_DEBUGGER_E2E_ENTRY is required') + +const { register } = await import(pathToFileURL(entryPath).href) +if (typeof register !== 'function') { + throw new Error(`No public register() export found at ${entryPath}`) +} + +const handle = register({ + mode: 'legacy', + devtools: { open: false }, + legacy: { serverPort: 0 } +}) +const ready = await handle.ready +if (ready.mode !== 'legacy') { + throw new Error(`Expected forced Legacy adapter, received ${ready.mode}`) +} + +const require = createRequire(import.meta.url) +const fixtureDirectory = dirname(fileURLToPath(import.meta.url)) +const { installScenarioController } = require(resolve(fixtureDirectory, 'scenario-runner.cjs')) +await installScenarioController({ handle, ready, consumer: 'esm' }) diff --git a/packages/network-debugger/test/e2e/legacy/fixtures/origin-server.mjs b/packages/network-debugger/test/e2e/legacy/fixtures/origin-server.mjs new file mode 100644 index 0000000..a29fde1 --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/fixtures/origin-server.mjs @@ -0,0 +1,234 @@ +import http from 'node:http' +import { gzipSync } from 'node:zlib' + +import { WebSocketServer } from 'ws' + +const BINARY_BODY = Buffer.from([0x00, 0x01, 0x02, 0x7f, 0x80, 0xfe, 0xff, 0x42]) + +async function readBody(request) { + const chunks = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + return Buffer.concat(chunks) +} + +function closeHttpServer(server, sockets) { + return new Promise((resolve, reject) => { + server.close((error) => { + if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error) + else resolve() + }) + for (const socket of sockets) socket.destroy() + }) +} + +/** + * A real loopback origin used by the Legacy protocol suite. Test control stays + * on process IPC; every entry recorded here is business traffic emitted by the + * packaged consumer. + */ +export async function startLegacyOriginServer() { + const sockets = new Set() + const records = [] + const webSocketServer = new WebSocketServer({ + noServer: true, + perMessageDeflate: { + threshold: 0, + serverNoContextTakeover: true, + clientNoContextTakeover: true + } + }) + + const server = http.createServer(async (request, response) => { + const url = new URL(request.url, `http://${request.headers.host ?? '127.0.0.1'}`) + const token = url.searchParams.get('token') ?? '' + const record = { + sequence: records.length + 1, + timestamp: new Date().toISOString(), + protocol: 'http', + method: request.method, + url: url.href, + token + } + records.push(record) + + try { + if (url.pathname === '/get' || url.pathname === '/concurrent') { + const body = `get-response:${token}` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-e2e-token': token + }) + response.end(body) + return + } + + if (url.pathname === '/post') { + const requestBody = await readBody(request) + const body = JSON.stringify({ token, requestBody: requestBody.toString('utf8') }) + response.writeHead(201, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-e2e-token': token + }) + response.end(body) + return + } + + if (url.pathname === '/text') { + const body = `legacy-text:${token}:你好` + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body) + }) + response.end(body) + return + } + + if (url.pathname === '/gzip') { + const decoded = `legacy-gzip:${token}:压缩内容` + const body = gzipSync(decoded) + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-encoding': 'gzip', + 'content-length': body.length + }) + response.end(body) + return + } + + if (url.pathname === '/binary') { + response.writeHead(200, { + 'content-type': 'application/octet-stream', + 'content-length': BINARY_BODY.length + }) + response.end(BINARY_BODY) + return + } + + if (url.pathname === '/redirect') { + response.writeHead(302, { + location: `/text?token=${encodeURIComponent(token)}`, + 'content-length': '0' + }) + response.end() + return + } + + if (url.pathname === '/reset') { + request.socket.destroy(new Error(`intentional reset:${token}`)) + return + } + + if (url.pathname === '/timeout') { + // The consumer owns the timeout and destroys this socket. Keeping the + // origin passive makes the failure a real client timeout. + return + } + + if (url.pathname === '/abort') { + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'transfer-encoding': 'chunked' + }) + response.flushHeaders() + response.write(`partial:${token}:`) + return + } + + if (url.pathname === '/sse') { + response.writeHead(200, { + 'content-type': 'text/event-stream; charset=utf-8', + 'cache-control': 'no-cache', + connection: 'close' + }) + response.flushHeaders() + + // Deliberately split one logical event across transport chunks. This + // verifies streaming parsing rather than parsing a manufactured body. + const chunks = [ + 'id: 1\nevent: alpha\ndata: first', + '\ndata: line-2\n\n', + 'id: 2\ndata: second\n\n', + 'event: omega\nid: 3\ndata: third\n\n' + ] + const writeNext = () => { + const chunk = chunks.shift() + if (chunk === undefined) { + response.end() + return + } + response.write(chunk) + setImmediate(writeNext) + } + writeNext() + return + } + + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('not found') + } catch (error) { + record.error = error instanceof Error ? error.message : String(error) + if (!response.headersSent) response.writeHead(500) + response.end(String(error)) + } + }) + + server.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => sockets.delete(socket)) + }) + + server.on('upgrade', (request, socket, head) => { + const url = new URL(request.url, `http://${request.headers.host ?? '127.0.0.1'}`) + if (url.pathname !== '/websocket') { + socket.destroy() + return + } + records.push({ + sequence: records.length + 1, + timestamp: new Date().toISOString(), + protocol: 'websocket', + method: request.method, + url: url.href, + token: url.searchParams.get('token') ?? '' + }) + webSocketServer.handleUpgrade(request, socket, head, (webSocket) => { + webSocketServer.emit('connection', webSocket, request) + }) + }) + + webSocketServer.on('connection', (socket) => { + socket.on('message', (data, isBinary) => { + socket.send(data, { binary: isBinary }) + }) + }) + + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + + const address = server.address() + if (!address || typeof address === 'string') { + await closeHttpServer(server, sockets) + throw new Error('Legacy origin fixture did not expose a TCP address') + } + + let closed = false + return { + baseUrl: `http://127.0.0.1:${address.port}`, + binaryBody: BINARY_BODY, + records, + async close() { + if (closed) return + closed = true + for (const client of webSocketServer.clients) client.terminate() + webSocketServer.close() + await closeHttpServer(server, sockets) + } + } +} diff --git a/packages/network-debugger/test/e2e/legacy/fixtures/scenario-runner.cjs b/packages/network-debugger/test/e2e/legacy/fixtures/scenario-runner.cjs new file mode 100644 index 0000000..c4c3b88 --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/fixtures/scenario-runner.cjs @@ -0,0 +1,296 @@ +'use strict' + +const http = require('node:http') +const WebSocket = require('ws') + +function serializeError(error) { + return { + name: error?.name ?? 'Error', + message: error?.message ?? String(error), + code: error?.code + } +} + +function httpRequest(url, { method = 'GET', body, headers = {} } = {}) { + return new Promise((resolve, reject) => { + const request = http.request(url, { method, headers }, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolve({ + status: response.statusCode, + headers: response.headers, + bodyBase64: Buffer.concat(chunks).toString('base64') + }) + }) + }) + request.once('error', reject) + if (Array.isArray(body)) { + for (const chunk of body.slice(0, -1)) request.write(chunk) + request.end(body.at(-1)) + } else { + request.end(body) + } + }) +} + +function failedHttpRequest(url, { timeoutMs } = {}) { + return new Promise((resolve, reject) => { + let settled = false + const request = http.request(url, { method: 'GET' }, (response) => { + response.resume() + response.once('error', (error) => { + if (settled) return + settled = true + resolve({ failed: true, error: serializeError(error) }) + }) + response.once('end', () => { + if (settled) return + settled = true + reject(new Error(`Expected ${url} to fail, but it completed with ${response.statusCode}`)) + }) + }) + request.once('error', (error) => { + if (settled) return + settled = true + resolve({ failed: true, error: serializeError(error) }) + }) + if (timeoutMs !== undefined) { + request.setTimeout(timeoutMs, () => { + request.destroy(new Error(`intentional client timeout after ${timeoutMs}ms`)) + }) + } + request.once('close', () => { + if (settled) return + settled = true + resolve({ failed: true, error: { name: 'Error', message: 'request closed' } }) + }) + request.once('response', () => { + // A reset/timeout scenario must not unexpectedly complete successfully. + request.once('finish', () => undefined) + }) + request.once('abort', reject) + request.end() + }) +} + +function abortResponse(url) { + return new Promise((resolve, reject) => { + let settled = false + const request = http.request(url, { method: 'GET' }, (response) => { + response.once('data', (chunk) => { + if (settled) return + settled = true + response.destroy(new Error('intentional response abort')) + request.destroy() + resolve({ aborted: true, firstChunk: Buffer.from(chunk).toString('utf8') }) + }) + response.once('error', (error) => { + if (settled) return + settled = true + resolve({ aborted: true, error: serializeError(error) }) + }) + }) + request.once('error', (error) => { + if (settled) return + settled = true + resolve({ aborted: true, error: serializeError(error) }) + }) + request.once('close', () => { + if (settled) return + settled = true + resolve({ aborted: true }) + }) + request.once('abort', reject) + request.end() + }) +} + +function nextWebSocketMessage(socket) { + return new Promise((resolve, reject) => { + const onMessage = (data, isBinary) => { + cleanup() + resolve({ data: Buffer.from(data), isBinary }) + } + const onError = (error) => { + cleanup() + reject(error) + } + const onClose = (code) => { + cleanup() + reject(new Error(`WebSocket closed before a message (code=${code})`)) + } + const cleanup = () => { + socket.off('message', onMessage) + socket.off('error', onError) + socket.off('close', onClose) + } + socket.once('message', onMessage) + socket.once('error', onError) + socket.once('close', onClose) + }) +} + +async function webSocketRoundTrip(url, token, binaryBase64) { + const socket = new WebSocket(url, { + perMessageDeflate: { + threshold: 0, + serverNoContextTakeover: true, + clientNoContextTakeover: true + } + }) + await new Promise((resolve, reject) => { + socket.once('open', resolve) + socket.once('error', reject) + }) + + const text = `client-text:${token}` + const textMessage = nextWebSocketMessage(socket) + socket.send(text) + const textEcho = await textMessage + + const binary = Buffer.from(binaryBase64, 'base64') + const binaryMessage = nextWebSocketMessage(socket) + socket.send(binary) + const binaryEcho = await binaryMessage + + const closed = new Promise((resolve, reject) => { + socket.once('close', (code, reason) => resolve({ code, reason: reason.toString() })) + socket.once('error', reject) + }) + socket.close(1000, 'scenario-complete') + const close = await closed + + return { + text, + textEcho: textEcho.data.toString('utf8'), + textEchoIsBinary: textEcho.isBinary, + binaryBase64, + binaryEchoBase64: binaryEcho.data.toString('base64'), + binaryEchoIsBinary: binaryEcho.isBinary, + extensions: socket.extensions, + close + } +} + +async function runScenario(message) { + switch (message.scenario) { + case 'http-get': + case 'body': + return httpRequest(message.url, { + headers: { 'x-e2e-token': message.token } + }) + case 'fetch-post': { + const requestBody = `fetch-request:${message.token}` + const response = await fetch(message.url, { + method: 'POST', + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'x-e2e-token': message.token + }, + body: requestBody + }) + return { + status: response.status, + body: await response.text(), + requestBody + } + } + case 'http-post-chunks': { + const chunks = [`http-`, `request:`, message.token] + const response = await httpRequest(message.url, { + method: 'POST', + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'x-e2e-token': message.token + }, + body: chunks + }) + return { ...response, requestBody: chunks.join('') } + } + case 'redirect': { + const redirect = await httpRequest(message.url, { + headers: { 'x-e2e-token': message.token } + }) + const location = redirect.headers.location + if (!location) throw new Error('redirect response did not include Location') + const destinationUrl = new URL(location, message.url).href + const destination = await httpRequest(destinationUrl, { + headers: { 'x-e2e-token': message.token } + }) + return { redirect, destination, destinationUrl } + } + case 'reset': + return failedHttpRequest(message.url) + case 'timeout': + return failedHttpRequest(message.url, { timeoutMs: 150 }) + case 'abort': + return abortResponse(message.url) + case 'sse': { + const response = await fetch(message.url, { + headers: { 'x-e2e-token': message.token } + }) + return { status: response.status, body: await response.text() } + } + case 'websocket': + return webSocketRoundTrip(message.url, message.token, message.binaryBase64) + case 'concurrent': + return Promise.all( + message.urls.map((url) => httpRequest(url, { headers: { 'x-e2e-token': message.token } })) + ) + default: + throw new Error(`Unknown Legacy E2E scenario: ${message.scenario}`) + } +} + +function send(message) { + return new Promise((resolve, reject) => { + if (!process.send) { + reject(new Error('Legacy E2E consumer lost its IPC channel')) + return + } + process.send(message, (error) => (error ? reject(error) : resolve())) + }) +} + +async function installScenarioController({ handle, ready, consumer }) { + if (!process.send) throw new Error('Legacy E2E consumer must be started with process IPC') + + const onMessage = async (message) => { + if (message?.type === 'run') { + try { + const result = await runScenario(message) + await send({ type: 'scenario-result', id: message.id, ok: true, result }) + } catch (error) { + await send({ + type: 'scenario-result', + id: message.id, + ok: false, + error: error instanceof Error ? error.stack : String(error) + }) + } + return + } + + if (message?.type === 'shutdown') { + process.off('message', onMessage) + try { + await handle.dispose() + await send({ type: 'shutdown-complete' }) + } finally { + process.disconnect() + } + } + } + + process.on('message', onMessage) + await send({ + type: 'ready', + pid: process.pid, + consumer, + sessionInfo: ready + }) +} + +module.exports = { installScenarioController } diff --git a/packages/network-debugger/test/e2e/legacy/legacy-harness.mjs b/packages/network-debugger/test/e2e/legacy/legacy-harness.mjs new file mode 100644 index 0000000..f5e7f8e --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/legacy-harness.mjs @@ -0,0 +1,441 @@ +import { spawn } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { access, mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { CdpClient } from '../harness/cdp-client.mjs' +import { startLegacyOriginServer } from './fixtures/origin-server.mjs' + +const LEGACY_DIRECTORY = dirname(fileURLToPath(import.meta.url)) +const PACKAGE_DIRECTORY = resolve(LEGACY_DIRECTORY, '../../..') +const DEFAULT_TIMEOUT_MS = 12_000 + +const CONSUMERS = { + esm: { + entryPath: resolve(PACKAGE_DIRECTORY, 'dist/index.mjs'), + fixturePath: resolve(LEGACY_DIRECTORY, 'fixtures/consumer.mjs') + }, + cjs: { + entryPath: resolve(PACKAGE_DIRECTORY, 'dist/index.js'), + fixturePath: resolve(LEGACY_DIRECTORY, 'fixtures/consumer.cjs') + } +} + +function serializeError(error) { + if (error instanceof Error) { + return { name: error.name, message: error.message, stack: error.stack } + } + return { message: String(error) } +} + +function waitForExit(child, timeoutMs, label) { + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.resolve({ code: child.exitCode, signal: child.signalCode }) + } + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error(`Timed out after ${timeoutMs}ms while waiting for ${label}`)) + }, timeoutMs) + const onExit = (code, signal) => { + cleanup() + resolvePromise({ code, signal }) + } + const cleanup = () => { + clearTimeout(timer) + child.off('exit', onExit) + } + child.once('exit', onExit) + }) +} + +async function fetchJson(url, label) { + let response + try { + response = await fetch(url, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) }) + } catch (error) { + throw new Error(`Could not read Legacy ${label} from ${url}: ${error}`, { cause: error }) + } + const body = await response.text() + if (!response.ok) { + throw new Error(`Legacy ${label} returned HTTP ${response.status} from ${url}: ${body}`) + } + try { + return JSON.parse(body) + } catch (error) { + throw new Error(`Legacy ${label} was not JSON at ${url}: ${body}`, { cause: error }) + } +} + +function assertStandardTarget(target) { + if (!target || typeof target !== 'object') { + throw new Error(`registration.ready did not return a target: ${JSON.stringify(target)}`) + } + for (const field of [ + 'id', + 'title', + 'type', + 'webSocketDebuggerUrl', + 'devtoolsFrontendUrl', + 'discoveryUrl' + ]) { + if (typeof target[field] !== 'string' || target[field].length === 0) { + throw new Error(`Legacy target.${field} must be a non-empty string`) + } + } + if (!/^ws:\/\/127\.0\.0\.1:\d+\//.test(target.webSocketDebuggerUrl)) { + throw new Error( + `Legacy target must expose a loopback WebSocket path: ${target.webSocketDebuggerUrl}` + ) + } + if (!/^http:\/\/127\.0\.0\.1:\d+\/json\/list$/.test(target.discoveryUrl)) { + throw new Error( + `Legacy target must expose standard /json/list discovery: ${target.discoveryUrl}` + ) + } +} + +export class LegacyE2EHarness { + constructor({ consumer = 'esm', entryPath } = {}) { + const definition = CONSUMERS[consumer] + if (!definition) throw new Error(`Unknown Legacy consumer kind: ${consumer}`) + this.consumer = consumer + this.entryPath = + entryPath ?? + process.env[`NETWORK_DEBUGGER_E2E_ENTRY_${consumer.toUpperCase()}`] ?? + definition.entryPath + this.fixturePath = definition.fixturePath + this.targetMessages = [] + this.stdout = '' + this.stderr = '' + this.closed = false + } + + async start() { + await access(this.entryPath).catch(() => { + throw new Error( + `Built ${this.consumer.toUpperCase()} package entry is missing at ${this.entryPath}. ` + + 'Run `pnpm --filter node-network-devtools build` before the Legacy E2E suite.' + ) + }) + + this.origin = await startLegacyOriginServer() + const child = spawn(process.execPath, [this.fixturePath], { + cwd: PACKAGE_DIRECTORY, + env: { + ...process.env, + NETWORK_DEBUGGER_E2E_ENTRY: this.entryPath + }, + stdio: ['ignore', 'pipe', 'pipe', 'ipc'] + }) + this.child = child + + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + this.stdout += chunk + }) + child.stderr.on('data', (chunk) => { + this.stderr += chunk + }) + child.on('message', (message) => this.targetMessages.push(message)) + + const ready = await this.waitForTargetMessage((message) => message?.type === 'ready') + this.targetPid = ready.pid + this.sessionInfo = ready.sessionInfo + if (ready.consumer !== this.consumer) { + throw new Error(`Expected ${this.consumer} consumer, target reported ${ready.consumer}`) + } + if (this.sessionInfo?.mode !== 'legacy') { + throw new Error(`Forced Legacy selected ${this.sessionInfo?.mode ?? 'no adapter'}`) + } + assertStandardTarget(this.sessionInfo.target) + + const discoveryUrl = new URL(this.sessionInfo.target.discoveryUrl) + this.discovery = { + list: await fetchJson(discoveryUrl.href, '/json/list'), + version: await fetchJson(new URL('/json/version', discoveryUrl).href, '/json/version'), + protocol: await fetchJson(new URL('/json/protocol', discoveryUrl).href, '/json/protocol') + } + + if (!Array.isArray(this.discovery.list)) { + throw new Error('Legacy /json/list response must be an array') + } + this.discoveredTarget = this.discovery.list.find( + (target) => + target?.id === this.sessionInfo.target.id || + target?.webSocketDebuggerUrl === this.sessionInfo.target.webSocketDebuggerUrl + ) + if (!this.discoveredTarget) { + throw new Error( + `registration.ready target is absent from /json/list: ${JSON.stringify(this.discovery.list)}` + ) + } + for (const field of ['id', 'title', 'type', 'url', 'webSocketDebuggerUrl']) { + if (this.discoveredTarget[field] !== this.sessionInfo.target[field]) { + throw new Error(`registration.ready and /json/list disagree on ${field}`) + } + } + if (!Array.isArray(this.discovery.protocol?.domains)) { + throw new Error('Legacy /json/protocol must expose a domains array') + } + + const client = new CdpClient(this.sessionInfo.target.webSocketDebuggerUrl) + this.client = client + await client.connect() + this.networkEnable = await client.command('Network.enable') + return this + } + + createToken(prefix) { + return `${prefix}-${randomUUID()}` + } + + url(pathname, token, search = {}) { + const url = new URL(pathname, this.origin.baseUrl) + url.searchParams.set('token', token) + for (const [name, value] of Object.entries(search)) { + url.searchParams.set(name, String(value)) + } + return url.href + } + + webSocketUrl(pathname, token) { + const url = new URL(this.url(pathname, token)) + url.protocol = 'ws:' + return url.href + } + + async runScenario(scenario, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const id = randomUUID() + const resultPromise = this.waitForTargetMessage( + (message) => message?.type === 'scenario-result' && message.id === id, + { timeoutMs } + ) + + await new Promise((resolvePromise, reject) => { + this.child.send({ type: 'run', id, scenario, ...payload }, (error) => + error ? reject(error) : resolvePromise() + ) + }) + + const message = await resultPromise + if (!message.ok) throw new Error(`Legacy target scenario ${scenario} failed:\n${message.error}`) + return message.result + } + + waitForTargetMessage(predicate, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { + const existing = this.targetMessages.find(predicate) + if (existing) return Promise.resolve(existing) + if (this.child.exitCode !== null || this.child.signalCode !== null) { + return Promise.reject( + new Error( + `Legacy target already exited (code=${this.child.exitCode}, signal=${this.child.signalCode}).\n` + + this.stderr + ) + ) + } + + return new Promise((resolvePromise, reject) => { + const timer = setTimeout(() => { + cleanup() + reject( + new Error( + `Timed out after ${timeoutMs}ms waiting for Legacy target IPC. stderr:\n${this.stderr}` + ) + ) + }, timeoutMs) + const onMessage = (message) => { + let matches + try { + matches = predicate(message) + } catch (error) { + cleanup() + reject(error) + return + } + if (!matches) return + cleanup() + resolvePromise(message) + } + const onExit = (code, signal) => { + cleanup() + reject( + new Error( + `Legacy target exited while waiting for IPC (code=${code}, signal=${signal}).\n` + + this.stderr + ) + ) + } + const onError = (error) => { + cleanup() + reject(error) + } + const cleanup = () => { + clearTimeout(timer) + this.child.off('message', onMessage) + this.child.off('exit', onExit) + this.child.off('error', onError) + } + this.child.on('message', onMessage) + this.child.once('exit', onExit) + this.child.once('error', onError) + }) + } + + async writeFailureArtifacts(error) { + const configuredRoot = process.env.NETWORK_DEBUGGER_E2E_ARTIFACT_DIR + let artifactDirectory + if (configuredRoot) { + await mkdir(configuredRoot, { recursive: true }) + artifactDirectory = join( + configuredRoot, + `legacy-${this.consumer}-${Date.now()}-${process.pid}-${randomUUID()}` + ) + await mkdir(artifactDirectory, { recursive: true }) + } else { + artifactDirectory = await mkdtemp( + join(tmpdir(), `node-network-devtools-legacy-${this.consumer}-e2e-`) + ) + } + + await Promise.all([ + this.client?.writeJournal(join(artifactDirectory, 'cdp.ndjson')) ?? + writeFile(join(artifactDirectory, 'cdp.ndjson'), '', 'utf8'), + writeFile(join(artifactDirectory, 'consumer.stdout.log'), this.stdout, 'utf8'), + writeFile(join(artifactDirectory, 'consumer.stderr.log'), this.stderr, 'utf8'), + writeFile( + join(artifactDirectory, 'consumer-messages.ndjson'), + this.targetMessages.map((message) => JSON.stringify(message)).join('\n') + '\n', + 'utf8' + ), + writeFile( + join(artifactDirectory, 'origin.ndjson'), + (this.origin?.records ?? []).map((record) => JSON.stringify(record)).join('\n') + '\n', + 'utf8' + ), + writeFile( + join(artifactDirectory, 'target.json'), + JSON.stringify( + { + registrationTarget: this.sessionInfo?.target, + discoveredTarget: this.discoveredTarget, + discovery: this.discovery + }, + null, + 2 + ), + 'utf8' + ), + writeFile( + join(artifactDirectory, 'metadata.json'), + JSON.stringify( + { + error: serializeError(error), + node: process.version, + platform: process.platform, + consumer: this.consumer, + entryPath: this.entryPath, + targetPid: this.targetPid, + sessionInfo: this.sessionInfo + }, + null, + 2 + ), + 'utf8' + ) + ]) + + return artifactDirectory + } + + async close() { + if (this.closed) return + this.closed = true + const errors = [] + + try { + await this.client?.close() + } catch (error) { + errors.push(error) + } + + if ( + this.child && + this.child.connected && + this.child.exitCode === null && + this.child.signalCode === null + ) { + try { + const shutdown = this.waitForTargetMessage( + (message) => message?.type === 'shutdown-complete', + { timeoutMs: 4_000 } + ) + await new Promise((resolvePromise, reject) => { + this.child.send({ type: 'shutdown' }, (error) => + error ? reject(error) : resolvePromise() + ) + }) + await shutdown + } catch (error) { + errors.push(error) + } + } + + if (this.child && this.child.exitCode === null && this.child.signalCode === null) { + try { + await waitForExit(this.child, 3_000, 'Legacy target to exit') + } catch (error) { + errors.push(error) + this.child.kill('SIGTERM') + try { + await waitForExit(this.child, 2_000, 'Legacy target to terminate') + } catch (terminationError) { + errors.push(terminationError) + this.child.kill('SIGKILL') + } + } + } + + try { + await this.origin?.close() + } catch (error) { + errors.push(error) + } + + if (errors.length) throw new AggregateError(errors, 'Failed to clean up Legacy E2E harness') + } +} + +export async function withLegacyHarness(testContext, options, callback) { + const harness = new LegacyE2EHarness(options) + testContext.after(async () => { + try { + await harness.close() + } catch (error) { + const artifactDirectory = await harness.writeFailureArtifacts(error).catch(() => undefined) + if (artifactDirectory) { + console.error(`Legacy E2E cleanup failure artifacts: ${artifactDirectory}`) + } + throw error + } + }) + + try { + await harness.start() + return await callback(harness) + } catch (error) { + const artifactDirectory = await harness.writeFailureArtifacts(error).catch(() => undefined) + if (artifactDirectory) { + console.error(`Legacy E2E failure artifacts: ${artifactDirectory}`) + if (error instanceof Error) { + error.message = `${error.message}\nFailure artifacts: ${artifactDirectory}` + } else { + error = new Error(`${String(error)}\nFailure artifacts: ${artifactDirectory}`) + } + } + throw error + } +} diff --git a/packages/network-debugger/test/e2e/legacy/legacy-protocol.test.mjs b/packages/network-debugger/test/e2e/legacy/legacy-protocol.test.mjs new file mode 100644 index 0000000..df2f63c --- /dev/null +++ b/packages/network-debugger/test/e2e/legacy/legacy-protocol.test.mjs @@ -0,0 +1,623 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { withLegacyHarness } from './legacy-harness.mjs' + +const TEST_TIMEOUT_MS = 50_000 +const BINARY_WEBSOCKET_PAYLOAD = Buffer.from([0x00, 0xff, 0x10, 0x80, 0x42]) + +function eventUrl(params) { + return params.request?.url ?? params.response?.url ?? params.url +} + +function eventForUrl(client, method, url) { + return client.waitForEvent(method, (params) => eventUrl(params) === url) +} + +function assertTimestamp(value, label) { + assert.equal(typeof value, 'number', `${label} must be a number`) + assert.ok(Number.isFinite(value), `${label} must be finite`) + assert.ok(value >= 0, `${label} must not be negative`) +} + +function assertSuccessfulLifecycle(request, response, finished) { + const requestId = request.message.params.requestId + assert.ok(requestId, 'requestWillBeSent must include requestId') + assert.equal(response.message.params.requestId, requestId) + assert.equal(finished.message.params.requestId, requestId) + assert.ok(request.sequence < response.sequence, 'request must precede response') + assert.ok(response.sequence < finished.sequence, 'response must precede loadingFinished') + assertTimestamp(request.message.params.timestamp, 'request timestamp') + assertTimestamp(response.message.params.timestamp, 'response timestamp') + assertTimestamp(finished.message.params.timestamp, 'loadingFinished timestamp') +} + +function decodeBody(result) { + return result.base64Encoded + ? Buffer.from(result.body, 'base64') + : Buffer.from(result.body, 'utf8') +} + +async function successfulExchange(harness, scenario, { url, token, ...payload }) { + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const scenarioPromise = harness.runScenario(scenario, { url, token, ...payload }) + const request = await requestPromise + const requestId = request.message.params.requestId + const response = await harness.client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === requestId + ) + const finished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ) + const result = await scenarioPromise + assertSuccessfulLifecycle(request, response, finished) + for (const method of [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.loadingFinished' + ]) { + assert.equal( + harness.client.findEvents(method, (params) => params.requestId === requestId).length, + 1, + `${method} must be emitted exactly once for ${url}` + ) + } + return { request, response, finished, result } +} + +async function responseBody(harness, requestId) { + const exchange = await harness.client.command('Network.getResponseBody', { requestId }) + assert.equal(exchange.response.id, exchange.request.id) + assert.equal(typeof exchange.result?.body, 'string') + assert.equal(typeof exchange.result?.base64Encoded, 'boolean') + return exchange.result +} + +async function expectCdpError(client, method, params = {}) { + try { + await client.command(method, params, { timeoutMs: 3_000 }) + } catch (error) { + assert.ok(error.cdpResponse, `${method} must return a CDP error, not time out or disconnect`) + assert.equal(typeof error.cdpResponse.id, 'number') + assert.equal(typeof error.cdpError?.code, 'number') + assert.equal(typeof error.cdpError?.message, 'string') + assert.ok(error.cdpError.message.length > 0) + return error.cdpResponse + } + assert.fail(`${method} unexpectedly succeeded`) +} + +async function failedExchange(harness, scenario, url, token, { mayReceiveResponse = false } = {}) { + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const scenarioPromise = harness.runScenario(scenario, { url, token }) + const request = await requestPromise + const requestId = request.message.params.requestId + const failed = await harness.client.waitForEvent( + 'Network.loadingFailed', + (params) => params.requestId === requestId + ) + const result = await scenarioPromise + + assert.ok(failed.sequence > request.sequence) + assertTimestamp(failed.message.params.timestamp, `${scenario} loadingFailed timestamp`) + assert.equal(typeof failed.message.params.errorText, 'string') + assert.ok(failed.message.params.errorText.length > 0) + assert.equal( + harness.client.findEvents('Network.loadingFailed', (params) => params.requestId === requestId) + .length, + 1, + `${scenario} must emit loadingFailed exactly once` + ) + assert.equal( + harness.client.findEvents('Network.loadingFinished', (params) => params.requestId === requestId) + .length, + 0, + `${scenario} must not also emit loadingFinished` + ) + if (!mayReceiveResponse) { + assert.equal( + harness.client.findEvents( + 'Network.responseReceived', + (params) => params.requestId === requestId + ).length, + 0, + `${scenario} must not manufacture a response` + ) + } + return { request, failed, result } +} + +test( + 'Legacy exposes standard discovery and answers every raw CDP command id', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const target = harness.sessionInfo.target + assert.equal(target.type, 'node') + assert.equal(harness.discoveredTarget.id, target.id) + assert.equal(harness.discoveredTarget.webSocketDebuggerUrl, target.webSocketDebuggerUrl) + assert.equal(harness.networkEnable.response.id, harness.networkEnable.request.id) + assert.deepEqual(harness.networkEnable.result, {}) + assert.equal(typeof harness.discovery.version['Protocol-Version'], 'string') + assert.ok( + harness.discovery.protocol.domains.some((domain) => domain.domain === 'Network'), + '/json/protocol must describe Network' + ) + + const before = harness.client.journal.length + const debuggerOutcome = await Promise.allSettled([ + harness.client.command('Debugger.enable', {}, { timeoutMs: 3_000 }), + harness.client.command('Runtime.enable', {}, { timeoutMs: 3_000 }) + ]) + for (const outcome of debuggerOutcome) { + if (outcome.status === 'fulfilled') { + assert.ok( + Object.hasOwn(outcome.value.response, 'result'), + 'successful CDP command must include result' + ) + } else { + assert.ok( + outcome.reason?.cdpResponse, + `known unsupported domains must return CDP errors: ${outcome.reason}` + ) + } + } + const missingBody = await expectCdpError(harness.client, 'Network.getResponseBody', { + requestId: 'missing-request-id' + }) + assert.ok( + [-32602, -32000].includes(missingBody.error.code), + `missing body must use invalid-params/server-error, received ${missingBody.error.code}` + ) + const unknownMethod = await expectCdpError( + harness.client, + 'NodeNetworkDevtools.unknownMethod' + ) + assert.equal(unknownMethod.error.code, -32601) + + const commandJournal = harness.client.journal.slice(before) + const sent = commandJournal.filter( + (record) => record.direction === 'send' && record.message.id !== undefined + ) + const received = new Map( + commandJournal + .filter((record) => record.direction === 'receive' && record.message.id !== undefined) + .map((record) => [record.message.id, record.message]) + ) + assert.ok(sent.length >= 4) + for (const command of sent) { + const response = received.get(command.message.id) + assert.ok( + response, + `${command.message.method} (${command.message.id}) received no result or CDP error` + ) + const hasResult = Object.hasOwn(response, 'result') + const hasError = Object.hasOwn(response, 'error') + assert.notEqual( + hasResult, + hasError, + `${command.message.method} (${command.message.id}) must receive exactly one of result/error` + ) + } + }) + } +) + +test( + 'Legacy ESM consumer captures real HTTP GET and Fetch POST lifecycles', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const getToken = harness.createToken('http-get') + const getUrl = harness.url('/get', getToken) + const get = await successfulExchange(harness, 'http-get', { + url: getUrl, + token: getToken + }) + assert.equal(get.request.message.params.request.method, 'GET') + assert.equal(get.response.message.params.response.status, 200) + assert.equal( + Buffer.from(get.result.bodyBase64, 'base64').toString('utf8'), + `get-response:${getToken}` + ) + const getBody = await responseBody(harness, get.request.message.params.requestId) + assert.equal(decodeBody(getBody).toString('utf8'), `get-response:${getToken}`) + + const frames = get.request.message.params.initiator?.stack?.callFrames ?? [] + assert.ok(frames.length > 0, 'Legacy initiator capability must include real call frames') + assert.ok( + frames.some((frame) => + frame.url.replaceAll('\\', '/').endsWith('/legacy/fixtures/scenario-runner.cjs') + ), + `initiator must reference the real consumer scenario: ${JSON.stringify(frames)}` + ) + + const postToken = harness.createToken('fetch-post') + const postUrl = harness.url('/post', postToken) + const post = await successfulExchange(harness, 'fetch-post', { + url: postUrl, + token: postToken + }) + assert.equal(post.request.message.params.request.method, 'POST') + assert.equal(post.request.message.params.request.postData, `fetch-request:${postToken}`) + const postData = await harness.client.command('Network.getRequestPostData', { + requestId: post.request.message.params.requestId + }) + assert.equal(postData.result.postData, `fetch-request:${postToken}`) + assert.equal(post.response.message.params.response.status, 201) + const postBody = await responseBody(harness, post.request.message.params.requestId) + assert.deepEqual(JSON.parse(decodeBody(postBody).toString('utf8')), { + token: postToken, + requestBody: `fetch-request:${postToken}` + }) + assert.deepEqual(JSON.parse(post.result.body), { + token: postToken, + requestBody: `fetch-request:${postToken}` + }) + + const chunkedToken = harness.createToken('http-post-chunks') + const chunkedUrl = harness.url('/post', chunkedToken) + const chunked = await successfulExchange(harness, 'http-post-chunks', { + url: chunkedUrl, + token: chunkedToken + }) + const expectedChunkedBody = `http-request:${chunkedToken}` + assert.equal(chunked.request.message.params.request.postData, expectedChunkedBody) + const chunkedPostData = await harness.client.command('Network.getRequestPostData', { + requestId: chunked.request.message.params.requestId + }) + assert.equal(chunkedPostData.result.postData, expectedChunkedBody) + assert.deepEqual( + JSON.parse(Buffer.from(chunked.result.bodyBase64, 'base64').toString('utf8')), + { token: chunkedToken, requestBody: expectedChunkedBody } + ) + }) + } +) + +test( + 'Legacy preserves text, gzip, and binary response bodies', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const textToken = harness.createToken('text') + const text = await successfulExchange(harness, 'body', { + url: harness.url('/text', textToken), + token: textToken + }) + const textBody = await responseBody(harness, text.request.message.params.requestId) + assert.equal(textBody.base64Encoded, false) + assert.equal(decodeBody(textBody).toString('utf8'), `legacy-text:${textToken}:你好`) + + const gzipToken = harness.createToken('gzip') + const gzip = await successfulExchange(harness, 'body', { + url: harness.url('/gzip', gzipToken), + token: gzipToken + }) + const gzipBody = await responseBody(harness, gzip.request.message.params.requestId) + assert.equal(gzipBody.base64Encoded, false) + assert.equal(decodeBody(gzipBody).toString('utf8'), `legacy-gzip:${gzipToken}:压缩内容`) + + const binaryToken = harness.createToken('binary') + const binary = await successfulExchange(harness, 'body', { + url: harness.url('/binary', binaryToken), + token: binaryToken + }) + const binaryBody = await responseBody(harness, binary.request.message.params.requestId) + assert.equal(binaryBody.base64Encoded, true) + assert.deepEqual(decodeBody(binaryBody), harness.origin.binaryBody) + }) + } +) + +test( + 'Legacy reports redirect, abort, reset, and timeout terminal states', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const redirectToken = harness.createToken('redirect') + const redirectUrl = harness.url('/redirect', redirectToken) + const destinationUrl = harness.url('/text', redirectToken) + const firstRequestPromise = eventForUrl( + harness.client, + 'Network.requestWillBeSent', + redirectUrl + ) + const destinationRequestPromise = eventForUrl( + harness.client, + 'Network.requestWillBeSent', + destinationUrl + ) + const redirectScenario = harness.runScenario('redirect', { + url: redirectUrl, + token: redirectToken + }) + const firstRequest = await firstRequestPromise + const destinationRequest = await destinationRequestPromise + const firstResponse = await harness.client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === firstRequest.message.params.requestId + ) + const firstFinished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === firstRequest.message.params.requestId + ) + const destinationResponse = await harness.client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === destinationRequest.message.params.requestId + ) + const destinationFinished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === destinationRequest.message.params.requestId + ) + const redirectResult = await redirectScenario + + assertSuccessfulLifecycle(firstRequest, firstResponse, firstFinished) + assertSuccessfulLifecycle(destinationRequest, destinationResponse, destinationFinished) + assert.notEqual( + firstRequest.message.params.requestId, + destinationRequest.message.params.requestId + ) + assert.equal(firstResponse.message.params.response.status, 302) + assert.equal(destinationResponse.message.params.response.status, 200) + assert.equal(redirectResult.redirect.status, 302) + assert.equal(redirectResult.destination.status, 200) + assert.equal(redirectResult.destinationUrl, destinationUrl) + + const resetToken = harness.createToken('reset') + const reset = await failedExchange( + harness, + 'reset', + harness.url('/reset', resetToken), + resetToken + ) + assert.equal(reset.result.failed, true) + + const timeoutToken = harness.createToken('timeout') + const timeout = await failedExchange( + harness, + 'timeout', + harness.url('/timeout', timeoutToken), + timeoutToken + ) + assert.equal(timeout.result.failed, true) + + const abortToken = harness.createToken('abort') + const abort = await failedExchange( + harness, + 'abort', + harness.url('/abort', abortToken), + abortToken, + { mayReceiveResponse: true } + ) + assert.equal(abort.result.aborted, true) + }) + } +) + +test( + 'Legacy preserves SSE fields and ordering from a real chunked stream', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const token = harness.createToken('sse') + const url = harness.url('/sse', token) + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const scenarioPromise = harness.runScenario('sse', { url, token }) + const request = await requestPromise + const requestId = request.message.params.requestId + const response = await harness.client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === requestId + ) + const first = await harness.client.waitForEvent( + 'Network.eventSourceMessageReceived', + (params) => params.requestId === requestId && params.eventId === '1' + ) + const second = await harness.client.waitForEvent( + 'Network.eventSourceMessageReceived', + (params) => params.requestId === requestId && params.eventId === '2' + ) + const third = await harness.client.waitForEvent( + 'Network.eventSourceMessageReceived', + (params) => params.requestId === requestId && params.eventId === '3' + ) + const finished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ) + const result = await scenarioPromise + + assertSuccessfulLifecycle(request, response, finished) + assert.equal(response.message.params.type, 'EventSource') + assert.deepEqual( + [first, second, third].map(({ message }) => ({ + eventName: message.params.eventName, + eventId: message.params.eventId, + data: message.params.data + })), + [ + { eventName: 'alpha', eventId: '1', data: 'first\nline-2' }, + { eventName: 'message', eventId: '2', data: 'second' }, + { eventName: 'omega', eventId: '3', data: 'third' } + ] + ) + assert.ok(response.sequence < first.sequence) + assert.ok(first.sequence < second.sequence) + assert.ok(second.sequence < third.sequence) + assert.ok(third.sequence < finished.sequence) + assert.equal( + harness.client.findEvents( + 'Network.responseReceived', + (params) => params.requestId === requestId + ).length, + 1, + 'SSE must emit responseReceived exactly once' + ) + assert.equal( + harness.client.findEvents( + 'Network.eventSourceMessageReceived', + (params) => params.requestId === requestId + ).length, + 3, + 'SSE must emit exactly the three origin events' + ) + assert.equal( + harness.client.findEvents( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ).length, + 1, + 'SSE must terminate exactly once' + ) + assert.equal(result.status, 200) + assert.match(result.body, /event: alpha/) + }) + } +) + +test( + 'Legacy reports a real compressed WebSocket handshake, text, binary, and close events', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const token = harness.createToken('websocket') + const url = harness.webSocketUrl('/websocket', token) + const createdPromise = eventForUrl(harness.client, 'Network.webSocketCreated', url) + const scenarioPromise = harness.runScenario('websocket', { + url, + token, + binaryBase64: BINARY_WEBSOCKET_PAYLOAD.toString('base64') + }) + const created = await createdPromise + const requestId = created.message.params.requestId + const willSend = await harness.client.waitForEvent( + 'Network.webSocketWillSendHandshakeRequest', + (params) => params.requestId === requestId + ) + const handshake = await harness.client.waitForEvent( + 'Network.webSocketHandshakeResponseReceived', + (params) => params.requestId === requestId + ) + const textSent = await harness.client.waitForEvent( + 'Network.webSocketFrameSent', + (params) => + params.requestId === requestId && + params.response.opcode === 1 && + params.response.payloadData === `client-text:${token}` + ) + const textReceived = await harness.client.waitForEvent( + 'Network.webSocketFrameReceived', + (params) => + params.requestId === requestId && + params.response.opcode === 1 && + params.response.payloadData === `client-text:${token}` + ) + const binaryPayload = BINARY_WEBSOCKET_PAYLOAD.toString('base64') + const binarySent = await harness.client.waitForEvent( + 'Network.webSocketFrameSent', + (params) => + params.requestId === requestId && + params.response.opcode === 2 && + params.response.payloadData === binaryPayload + ) + const binaryReceived = await harness.client.waitForEvent( + 'Network.webSocketFrameReceived', + (params) => + params.requestId === requestId && + params.response.opcode === 2 && + params.response.payloadData === binaryPayload + ) + const closed = await harness.client.waitForEvent( + 'Network.webSocketClosed', + (params) => params.requestId === requestId + ) + const result = await scenarioPromise + const extensionHeader = Object.entries(handshake.message.params.response.headers).find( + ([name]) => name.toLowerCase() === 'sec-websocket-extensions' + )?.[1] + + assert.equal(handshake.message.params.response.status, 101) + assert.match(String(extensionHeader), /permessage-deflate/) + assert.ok(created.sequence < willSend.sequence) + assert.ok(willSend.sequence < handshake.sequence) + assert.ok(handshake.sequence < textSent.sequence) + assert.ok(textSent.sequence < textReceived.sequence) + assert.ok(textReceived.sequence < binarySent.sequence) + assert.ok(binarySent.sequence < binaryReceived.sequence) + assert.ok(binaryReceived.sequence < closed.sequence) + assert.equal(result.textEcho, `client-text:${token}`) + assert.equal(result.textEchoIsBinary, false) + assert.equal(result.binaryEchoBase64, binaryPayload) + assert.equal(result.binaryEchoIsBinary, true) + assert.equal(result.extensions, 'permessage-deflate') + assert.equal(result.close.code, 1000) + assert.equal( + harness.client.findEvents( + 'Network.loadingFailed', + (params) => params.requestId === requestId + ).length, + 0, + 'a successful WebSocket upgrade must not also report HTTP loadingFailed' + ) + }) + } +) + +test( + 'Legacy concurrent requests retain stable distinct request ids', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'esm' }, async (harness) => { + const token = harness.createToken('concurrent') + const urls = Array.from({ length: 20 }, (_value, index) => + harness.url('/concurrent', token, { index }) + ) + const requestPromises = urls.map((url) => + eventForUrl(harness.client, 'Network.requestWillBeSent', url) + ) + const scenarioPromise = harness.runScenario('concurrent', { urls, token }) + const requests = await Promise.all(requestPromises) + const terminals = await Promise.all( + requests.map(async (request) => { + const requestId = request.message.params.requestId + const response = await harness.client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === requestId + ) + const finished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ) + assertSuccessfulLifecycle(request, response, finished) + return requestId + }) + ) + const results = await scenarioPromise + + assert.equal(new Set(terminals).size, urls.length) + assert.equal(results.length, urls.length) + assert.ok(results.every((result) => result.status === 200)) + }) + } +) + +test( + 'Legacy works through the built CommonJS public package consumer', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withLegacyHarness(t, { consumer: 'cjs' }, async (harness) => { + assert.equal(harness.sessionInfo.mode, 'legacy') + const token = harness.createToken('cjs') + const exchange = await successfulExchange(harness, 'http-get', { + url: harness.url('/get', token), + token + }) + assert.equal(exchange.response.message.params.response.status, 200) + const body = await responseBody(harness, exchange.request.message.params.requestId) + assert.equal(decodeBody(body).toString('utf8'), `get-response:${token}`) + }) + } +) diff --git a/packages/network-debugger/test/e2e/native-protocol.test.mjs b/packages/network-debugger/test/e2e/native-protocol.test.mjs new file mode 100644 index 0000000..0dae291 --- /dev/null +++ b/packages/network-debugger/test/e2e/native-protocol.test.mjs @@ -0,0 +1,264 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import test from 'node:test' + +import { withNativeHarness } from './harness/native-harness.mjs' + +const TEST_TIMEOUT_MS = 30_000 + +function eventForUrl(client, method, url) { + return client.waitForEvent(method, (params) => { + const eventUrl = params.request?.url ?? params.response?.url + return eventUrl === url + }) +} + +function assertTimestamp(value, label) { + assert.equal(typeof value, 'number', `${label} must be a number`) + assert.ok(Number.isFinite(value), `${label} must be finite`) + assert.ok(value >= 0, `${label} must not be negative`) +} + +function assertNativeWallTime(value) { + assertTimestamp(value, 'request wallTime') + // Node's native Network domain currently emits epoch milliseconds on some + // releases even though CDP models wallTime as TimeSinceEpoch (seconds). + // NativeAdapter intentionally forwards the official runtime unchanged. + const epochMilliseconds = value > 100_000_000_000 ? value : value * 1_000 + assert.ok( + Math.abs(Date.now() - epochMilliseconds) < 60_000, + `wallTime must represent the current epoch; received ${value}` + ) +} + +function assertSuccessfulLifecycle(client, request, response, finished) { + const requestId = request.message.params.requestId + assert.ok(requestId, 'requestWillBeSent must include a requestId') + assert.equal(response.message.params.requestId, requestId) + assert.equal(finished.message.params.requestId, requestId) + assert.ok(request.sequence < response.sequence, 'request must precede response') + assert.ok(response.sequence < finished.sequence, 'response must precede loadingFinished') + + assertTimestamp(request.message.params.timestamp, 'request timestamp') + assertTimestamp(response.message.params.timestamp, 'response timestamp') + assertTimestamp(finished.message.params.timestamp, 'loadingFinished timestamp') + assert.ok( + request.message.params.timestamp <= response.message.params.timestamp, + 'request timestamp must not exceed response timestamp' + ) + assert.ok( + response.message.params.timestamp <= finished.message.params.timestamp, + 'response timestamp must not exceed loadingFinished timestamp' + ) + + assertNativeWallTime(request.message.params.wallTime) + for (const method of [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.loadingFinished' + ]) { + assert.equal( + client.findEvents(method, (params) => params.requestId === requestId).length, + 1, + `${method} must be emitted exactly once` + ) + } +} + +function decodeBody(result) { + return result.base64Encoded ? Buffer.from(result.body, 'base64').toString('utf8') : result.body +} + +test( + 'native adapter uses a real Node Inspector session with correlated CDP commands', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withNativeHarness(t, async (harness) => { + assert.equal(harness.sessionInfo.mode, 'native') + assert.deepEqual(harness.nativeFunctionsUnchanged, { + fetch: true, + httpRequest: true, + httpsRequest: true + }) + assert.equal(harness.sessionInfo.target.webSocketDebuggerUrl, harness.inspectorUrl) + assert.equal(harness.networkEnable.request.id, harness.networkEnable.response.id) + assert.deepEqual(harness.networkEnable.result, {}) + + const evaluation = await harness.client.command('Runtime.evaluate', { + expression: 'process.pid', + returnByValue: true + }) + assert.equal(evaluation.result.result.type, 'number') + assert.equal(evaluation.result.result.value, harness.targetPid) + }) + } +) + +test( + 'native adapter reports a real HTTP lifecycle and source initiator', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withNativeHarness(t, async (harness) => { + const token = harness.createToken('http') + const url = harness.url('/http', token) + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const responsePromise = eventForUrl(harness.client, 'Network.responseReceived', url) + + const scenarioPromise = harness.runScenario('http', { url, token }) + const request = await requestPromise + const response = await responsePromise + const finished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === request.message.params.requestId + ) + const scenario = await scenarioPromise + + assertSuccessfulLifecycle(harness.client, request, response, finished) + assert.equal(request.message.params.request.method, 'GET') + assert.equal(response.message.params.response.status, 200) + assert.equal(response.message.params.response.headers['x-e2e-token'], token) + assert.deepEqual(scenario, { status: 200, body: `http-response:${token}` }) + + if (harness.sessionInfo.capabilities?.initiator !== false) { + const frames = request.message.params.initiator?.stack?.callFrames ?? [] + assert.ok(frames.length > 0, 'supported initiator capability must include call frames') + assert.ok( + frames.some((frame) => + frame.url.replaceAll('\\', '/').endsWith('/test/e2e/fixtures/native-target.mjs') + ), + `initiator must reference native-target.mjs; received ${JSON.stringify(frames)}` + ) + } + }) + } +) + +test( + 'native adapter reports a real Fetch lifecycle and response body', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withNativeHarness(t, async (harness) => { + const token = harness.createToken('fetch') + const url = harness.url('/fetch', token) + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const responsePromise = eventForUrl(harness.client, 'Network.responseReceived', url) + + const scenarioPromise = harness.runScenario('fetch', { url, token }) + const request = await requestPromise + const response = await responsePromise + const finished = await harness.client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === request.message.params.requestId + ) + const scenario = await scenarioPromise + + assertSuccessfulLifecycle(harness.client, request, response, finished) + assert.equal(request.message.params.request.method, 'POST') + assert.equal(response.message.params.response.status, 201) + + const bodyExchange = await harness.client.command('Network.getResponseBody', { + requestId: request.message.params.requestId + }) + const body = JSON.parse(decodeBody(bodyExchange.result)) + assert.deepEqual(body, { + token, + requestBody: `fetch-request:${token}` + }) + assert.equal(scenario.status, 201) + assert.deepEqual(JSON.parse(scenario.body), body) + }) + } +) + +test( + 'native adapter terminates a failed request with loadingFailed only', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withNativeHarness(t, async (harness) => { + const token = harness.createToken('failed') + const url = harness.url('/reset', token) + const requestPromise = eventForUrl(harness.client, 'Network.requestWillBeSent', url) + const scenarioPromise = harness.runScenario('failed', { url, token }) + + const request = await requestPromise + const requestId = request.message.params.requestId + const failed = await harness.client.waitForEvent( + 'Network.loadingFailed', + (params) => params.requestId === requestId + ) + const scenario = await scenarioPromise + + assert.equal(scenario.failed, true) + assert.ok(failed.sequence > request.sequence) + assertTimestamp(failed.message.params.timestamp, 'loadingFailed timestamp') + assert.equal(typeof failed.message.params.errorText, 'string') + assert.ok(failed.message.params.errorText.length > 0) + assert.equal( + harness.client.findEvents( + 'Network.responseReceived', + (params) => params.requestId === requestId + ).length, + 0 + ) + assert.equal( + harness.client.findEvents( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ).length, + 0 + ) + }) + } +) + +test( + 'native adapter records a real backend-neutral Session and HAR', + { timeout: TEST_TIMEOUT_MS }, + async (t) => { + await withNativeHarness( + t, + async (harness) => { + assert.equal(harness.sessionInfo.mode, 'native') + assert.equal(harness.sessionInfo.session?.directory, harness.sessionDirectory) + + const token = harness.createToken('native-session') + const url = harness.url('/http', token) + const scenario = await harness.runScenario('http', { url, token }) + assert.deepEqual(scenario, { status: 200, body: `http-response:${token}` }) + + // The scenario result arrives over IPC, while Session events arrive over + // a separate Inspector WebSocket. Wait for the real terminal event and + // body capture instead of racing finalization against that second channel. + await harness.waitForRecordedRequest(url) + await harness.finalizeSession() + const manifest = JSON.parse( + await readFile(resolve(harness.sessionDirectory, 'manifest.json'), 'utf8') + ) + const har = JSON.parse( + await readFile(resolve(harness.sessionDirectory, 'session.har'), 'utf8') + ) + + assert.equal(manifest.state, 'completed') + assert.equal(manifest.target.id, harness.sessionInfo.target.id) + assert.equal(manifest.stats.requestCount, 1) + assert.equal(manifest.stats.failedRequestCount, 0) + assert.equal(manifest.stats.bodyErrorCount, 0) + assert.equal(manifest.stats.bodyCount, 1) + const indexedRequest = Object.values(manifest.requestIndex)[0] + assert.equal(indexedRequest.request.url, url) + const indexedBody = manifest.bodyIndex[indexedRequest.requestId] + const body = await readFile(resolve(harness.sessionDirectory, indexedBody.path)) + assert.equal(createHash('sha256').update(body).digest('hex'), indexedBody.sha256) + assert.equal(body.toString('utf8'), `http-response:${token}`) + + assert.equal(har.log.version, '1.2') + assert.equal(har.log.entries.length, 1) + assert.equal(har.log.entries[0].request.url, url) + assert.equal(har.log.entries[0].response.content.text, `http-response:${token}`) + }, + { recordSession: true } + ) + } +) diff --git a/packages/network-debugger/test/e2e/os-smoke/adapter-smoke-controller.mjs b/packages/network-debugger/test/e2e/os-smoke/adapter-smoke-controller.mjs new file mode 100644 index 0000000..c81aa3f --- /dev/null +++ b/packages/network-debugger/test/e2e/os-smoke/adapter-smoke-controller.mjs @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict' +import { readFile, realpath } from 'node:fs/promises' +import http from 'node:http' +import { createRequire } from 'node:module' +import { isAbsolute, join, relative } from 'node:path' + +const PACKAGE_NAME = 'node-network-devtools' +const RESULT_PREFIX = 'NND_OS_SMOKE_RESULT ' +const ROUNDS = Number(process.env.NND_OS_SMOKE_ROUNDS ?? 20) +const installationRoot = process.env.NND_OS_SMOKE_INSTALL_ROOT + +assert.ok(Number.isInteger(ROUNDS) && ROUNDS >= 20, 'OS smoke must run at least 20 rounds') +assert.ok(installationRoot, 'NND_OS_SMOKE_INSTALL_ROOT is required') +assert.ok( + process.execArgv.some((argument) => argument.startsWith('--experimental-network-inspection')), + 'the isolated controller requires --experimental-network-inspection' +) + +function requestJson(url, timeoutMs = 3_000) { + return new Promise((resolve, reject) => { + const request = http.get(url, { agent: false }, (response) => { + const chunks = [] + const socket = response.socket + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + const text = Buffer.concat(chunks).toString('utf8') + let result + let failure + + if (response.statusCode !== 200) { + failure = new Error(`${url} returned HTTP ${response.statusCode}: ${text}`) + } else { + try { + result = JSON.parse(text) + } catch (error) { + failure = new Error(`${url} returned invalid JSON: ${text}`, { cause: error }) + } + } + + // inspector.close() is synchronous. Do not call it while the discovery + // socket is still live; this is especially important on Windows. + const settle = () => (failure ? reject(failure) : resolve(result)) + if (socket.destroyed) { + settle() + } else { + socket.once('close', settle) + socket.destroy() + } + }) + }) + request.setTimeout(timeoutMs, () => request.destroy(new Error(`Timed out requesting ${url}`))) + request.once('error', reject) + }) +} + +async function assertEndpointClosed(url) { + let lastResponse + for (let attempt = 0; attempt < 10; attempt += 1) { + try { + lastResponse = await requestJson(url, 500) + } catch { + return + } + await new Promise((resolve) => setTimeout(resolve, 50)) + } + assert.fail( + `target endpoint remained open after dispose: ${url}\n${JSON.stringify(lastResponse)}` + ) +} + +async function runRounds(register, mode) { + const result = { rounds: 0, targetsDiscovered: 0, endpointsClosed: 0 } + + for (let round = 1; round <= ROUNDS; round += 1) { + let registration + let target + try { + registration = register({ + mode, + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false }, + legacy: { serverPort: 0 } + }) + const ready = await registration.ready + target = ready.target + + assert.equal(ready.mode, mode, `${mode} round ${round} selected another adapter`) + assert.equal(registration.status().state, 'ready') + assert.match(target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) + assert.match(target.webSocketDebuggerUrl, /^ws:\/\/127\.0\.0\.1:\d+\//) + + const targets = await requestJson(target.discoveryUrl) + assert.ok(Array.isArray(targets), `${mode} round ${round} discovery must be an array`) + assert.ok( + targets.some((candidate) => candidate.id === target.id), + `${mode} round ${round} target is absent from discovery` + ) + result.targetsDiscovered += 1 + } finally { + await registration?.dispose() + } + + assert.ok(registration, `${mode} round ${round} did not create a registration`) + assert.ok(target, `${mode} round ${round} did not expose a target`) + assert.equal(registration.status().state, 'disposed') + await assertEndpointClosed(target.discoveryUrl) + result.endpointsClosed += 1 + result.rounds += 1 + } + + return result +} + +const consumerRequire = createRequire(join(installationRoot, 'consumer.cjs')) +const packageDirectory = await realpath(join(installationRoot, 'node_modules', PACKAGE_NAME)) +const resolvedEntry = await realpath(consumerRequire.resolve(PACKAGE_NAME)) +const entryRelativeToPackage = relative(packageDirectory, resolvedEntry) +assert.ok( + entryRelativeToPackage && + !entryRelativeToPackage.startsWith('..') && + !isAbsolute(entryRelativeToPackage), + `resolved entry is outside the exact packed installation: ${resolvedEntry}` +) + +const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8')) +assert.equal(manifest.name, PACKAGE_NAME) +const { register } = consumerRequire(PACKAGE_NAME) +assert.equal(typeof register, 'function') + +const adapters = { + native: await runRounds(register, 'native'), + legacy: await runRounds(register, 'legacy') +} + +console.log( + `${RESULT_PREFIX}${JSON.stringify({ + schemaVersion: 1, + controllerPid: process.pid, + package: { name: manifest.name, version: manifest.version }, + rounds: ROUNDS, + adapters + })}` +) diff --git a/packages/network-debugger/test/e2e/os-smoke/adapter-smoke.test.mjs b/packages/network-debugger/test/e2e/os-smoke/adapter-smoke.test.mjs new file mode 100644 index 0000000..cd32ccc --- /dev/null +++ b/packages/network-debugger/test/e2e/os-smoke/adapter-smoke.test.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict' +import { execFile } from 'node:child_process' +import test from 'node:test' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' + +import { installPackedPackage, removePackedInstallation } from '../pack/packed-package.mjs' + +const execFileAsync = promisify(execFile) +const ROUNDS = Number(process.env.NND_OS_SMOKE_ROUNDS ?? 20) +const CHILD_TIMEOUT_MS = 240_000 +const TEST_TIMEOUT_MS = 300_000 +const RESULT_PREFIX = 'NND_OS_SMOKE_RESULT ' +const CONTROLLER_PATH = fileURLToPath(new URL('./adapter-smoke-controller.mjs', import.meta.url)) + +assert.ok(Number.isInteger(ROUNDS) && ROUNDS >= 20, 'OS smoke must run at least 20 rounds') + +async function runIsolatedController(installation) { + try { + return await execFileAsync( + process.execPath, + ['--experimental-network-inspection', CONTROLLER_PATH], + { + cwd: installation.root, + env: { + ...process.env, + NND_OS_SMOKE_INSTALL_ROOT: installation.root, + NND_OS_SMOKE_ROUNDS: String(ROUNDS) + }, + timeout: CHILD_TIMEOUT_MS, + killSignal: 'SIGKILL', + windowsHide: true, + maxBuffer: 10 * 1024 * 1024 + } + ) + } catch (error) { + const output = [error?.stdout, error?.stderr].filter(Boolean).join('\n') + throw new Error(`Isolated OS smoke controller failed:\n${output}`, { cause: error }) + } +} + +function parseResult(stdout) { + const resultLines = stdout.split(/\r?\n/).filter((line) => line.startsWith(RESULT_PREFIX)) + + assert.equal( + resultLines.length, + 1, + `expected one structured OS smoke result, received ${resultLines.length}:\n${stdout}` + ) + return JSON.parse(resultLines[0].slice(RESULT_PREFIX.length)) +} + +test( + 'an isolated packed-package controller survives 20 Native and Legacy rounds', + { timeout: TEST_TIMEOUT_MS }, + async () => { + const installation = await installPackedPackage('nnd-os-smoke-') + + try { + // Only the child loads the installed package. Waiting for execFile to + // close before cleanup avoids Windows file locks from loaded modules. + const { stdout } = await runIsolatedController(installation) + const result = parseResult(stdout) + + assert.equal(result.schemaVersion, 1) + assert.notEqual(result.controllerPid, process.pid) + assert.deepEqual(result.package, { + name: installation.manifest.name, + version: installation.manifest.version + }) + assert.equal(result.rounds, ROUNDS) + + for (const mode of ['native', 'legacy']) { + assert.deepEqual(result.adapters[mode], { + rounds: ROUNDS, + targetsDiscovered: ROUNDS, + endpointsClosed: ROUNDS + }) + } + } finally { + await removePackedInstallation(installation) + } + } +) diff --git a/packages/network-debugger/test/e2e/pack/legacy-runtime-consumer.cjs b/packages/network-debugger/test/e2e/pack/legacy-runtime-consumer.cjs new file mode 100644 index 0000000..7ee193e --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/legacy-runtime-consumer.cjs @@ -0,0 +1,71 @@ +'use strict' + +const http = require('node:http') +const { register } = require('node-network-devtools') + +if (!process.send) throw new Error('Legacy runtime consumer requires an IPC channel') + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => (error ? reject(error) : resolve())) + }) +} + +function request(url) { + return new Promise((resolve, reject) => { + const outgoing = http.get(url, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolve({ + status: response.statusCode, + bodyBase64: Buffer.concat(chunks).toString('base64') + }) + }) + }) + outgoing.once('error', reject) + }) +} + +async function main() { + const registration = register({ + mode: 'legacy', + devtools: { open: false }, + legacy: { serverPort: 0 } + }) + const ready = await registration.ready + await send({ type: 'ready', ready, pid: process.pid }) + + process.on('message', async (message) => { + if (message?.type === 'request') { + try { + await send({ type: 'request-result', id: message.id, result: await request(message.url) }) + } catch (error) { + await send({ + type: 'request-result', + id: message.id, + error: error instanceof Error ? error.stack : String(error) + }) + } + return + } + + if (message?.type === 'shutdown') { + await registration.dispose() + await send({ type: 'shutdown-complete' }) + process.removeAllListeners('message') + process.disconnect() + } + }) +} + +main().catch(async (error) => { + console.error(error) + try { + await send({ type: 'fatal', error: error instanceof Error ? error.stack : String(error) }) + } finally { + process.exitCode = 1 + process.disconnect() + } +}) diff --git a/packages/network-debugger/test/e2e/pack/legacy-runtime.test.mjs b/packages/network-debugger/test/e2e/pack/legacy-runtime.test.mjs new file mode 100644 index 0000000..468cb22 --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/legacy-runtime.test.mjs @@ -0,0 +1,443 @@ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { copyFile } from 'node:fs/promises' +import http from 'node:http' +import { createRequire } from 'node:module' +import test from 'node:test' +import { dirname, join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +import { installPackedPackage, removePackedInstallation } from './packed-package.mjs' + +const TEST_TIMEOUT_MS = 120_000 +const IO_TIMEOUT_MS = 10_000 +const FIXTURE_DIRECTORY = dirname(fileURLToPath(import.meta.url)) + +class CdpClient { + constructor(WebSocket, url) { + this.WebSocket = WebSocket + this.url = url + this.nextId = 0 + this.pending = new Map() + this.events = [] + this.waiters = new Set() + } + + async connect() { + const socket = new this.WebSocket(this.url) + this.socket = socket + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + socket.terminate() + reject(new Error(`Timed out connecting to ${this.url}`)) + }, IO_TIMEOUT_MS) + const cleanup = () => { + clearTimeout(timer) + socket.off('open', onOpen) + socket.off('error', onError) + } + const onOpen = () => { + cleanup() + resolve() + } + const onError = (error) => { + cleanup() + reject(error) + } + socket.once('open', onOpen) + socket.once('error', onError) + }) + socket.on('message', (data) => this.onMessage(data)) + socket.on('error', (error) => this.onDisconnect(error)) + socket.on('close', () => this.onDisconnect(new Error('CDP WebSocket closed'))) + return this + } + + command(method, params = {}) { + const id = ++this.nextId + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id) + reject(new Error(`Timed out waiting for ${method} (${id})`)) + }, IO_TIMEOUT_MS) + this.pending.set(id, { + resolve: (message) => { + clearTimeout(timer) + if (message.error) { + reject(new Error(`${method} failed: ${message.error.message}`)) + } else { + resolve(message.result) + } + }, + reject: (error) => { + clearTimeout(timer) + reject(error) + } + }) + this.socket.send(JSON.stringify({ id, method, params }), (error) => { + if (!error) return + const pending = this.pending.get(id) + this.pending.delete(id) + pending?.reject(error) + }) + }) + } + + waitForEvent(method, predicate = () => true) { + const existing = this.events.find( + (message) => message.method === method && predicate(message.params) + ) + if (existing) return Promise.resolve(existing.params) + + return new Promise((resolve, reject) => { + const waiter = { method, predicate, resolve, reject, timer: undefined } + waiter.timer = setTimeout(() => { + this.waiters.delete(waiter) + reject(new Error(`Timed out waiting for ${method}`)) + }, IO_TIMEOUT_MS) + this.waiters.add(waiter) + }) + } + + onMessage(data) { + const message = JSON.parse(data.toString()) + if (message.id !== undefined) { + const pending = this.pending.get(message.id) + if (!pending) return + this.pending.delete(message.id) + pending.resolve(message) + return + } + if (!message.method) return + this.events.push(message) + for (const waiter of [...this.waiters]) { + if (waiter.method !== message.method || !waiter.predicate(message.params)) continue + clearTimeout(waiter.timer) + this.waiters.delete(waiter) + waiter.resolve(message.params) + } + } + + onDisconnect(error) { + for (const pending of this.pending.values()) pending.reject(error) + this.pending.clear() + for (const waiter of this.waiters) { + clearTimeout(waiter.timer) + waiter.reject(error) + } + this.waiters.clear() + } + + async close() { + const socket = this.socket + if (!socket || socket.readyState === this.WebSocket.CLOSED) return + await new Promise((resolve) => { + const timer = setTimeout(() => { + socket.terminate() + resolve() + }, 2_000) + socket.once('close', () => { + clearTimeout(timer) + resolve() + }) + socket.close() + }) + } +} + +function listen(server) { + return new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) +} + +function closeServer(server) { + if (!server?.listening) return Promise.resolve() + return new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ) +} + +function requestBuffer(url, timeoutMs = IO_TIMEOUT_MS) { + return new Promise((resolve, reject) => { + const request = http.get(url, { agent: false }, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => resolve({ response, body: Buffer.concat(chunks) })) + }) + request.setTimeout(timeoutMs, () => request.destroy(new Error(`Timed out requesting ${url}`))) + request.once('error', reject) + }) +} + +function waitForMessage(child, messages, predicate, timeoutMs = IO_TIMEOUT_MS) { + const existing = messages.find(predicate) + if (existing) return Promise.resolve(existing) + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.reject(new Error(`Legacy consumer already exited with ${child.exitCode}`)) + } + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error('Timed out waiting for Legacy consumer IPC')) + }, timeoutMs) + const cleanup = () => { + clearTimeout(timer) + child.off('message', onMessage) + child.off('exit', onExit) + } + const onMessage = (message) => { + if (!predicate(message)) return + cleanup() + resolve(message) + } + const onExit = (code, signal) => { + cleanup() + reject(new Error(`Legacy consumer exited before IPC (code=${code}, signal=${signal})`)) + } + child.on('message', onMessage) + child.once('exit', onExit) + }) +} + +function send(child, message) { + return new Promise((resolve, reject) => + child.send(message, (error) => (error ? reject(error) : resolve())) + ) +} + +function waitForExit(child, timeoutMs = IO_TIMEOUT_MS) { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve() + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + child.off('exit', onExit) + reject(new Error('Timed out waiting for Legacy consumer to exit')) + }, timeoutMs) + const onExit = () => { + clearTimeout(timer) + resolve() + } + child.once('exit', onExit) + }) +} + +function runFinitePreload(packageDirectory, timeoutMs = 15_000) { + const registerUrl = pathToFileURL(join(packageDirectory, 'dist/register.mjs')).href + const source = + "process.stdout.write('@@NND_PACK_FINITE@@' + JSON.stringify({ pid: process.pid }) + '\\n')" + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [`--import=${registerUrl}`, '-e', source], { + cwd: packageDirectory, + env: { + ...process.env, + NND_MODE: 'legacy', + NND_LEGACY_SERVER_PORT: '0', + NND_PRELOAD_REPORT: '1' + }, + stdio: ['ignore', 'pipe', 'pipe'] + }) + let stdout = '' + let stderr = '' + let timedOut = false + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (stdout += chunk)) + child.stderr.on('data', (chunk) => (stderr += chunk)) + const timer = setTimeout(() => { + timedOut = true + child.kill('SIGKILL') + }, timeoutMs) + child.once('error', (error) => { + clearTimeout(timer) + reject(error) + }) + child.once('close', (code, signal) => { + clearTimeout(timer) + if (timedOut) { + reject( + new Error( + `Packed finite preload did not exit naturally on ${process.version}.\n` + + `stdout:\n${stdout}\nstderr:\n${stderr}` + ) + ) + return + } + resolve({ code, signal, stdout, stderr }) + }) + }) +} + +async function waitForEndpointClosed(url, timeoutMs = 5_000) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + await requestBuffer(url, 500) + } catch { + return + } + await new Promise((resolve) => setTimeout(resolve, 25)) + } + throw new Error(`Packed finite Legacy target remained reachable after exit: ${url}`) +} + +test( + 'the packed Legacy adapter runs a real CDP lifecycle under this Node runtime', + { timeout: TEST_TIMEOUT_MS }, + async () => { + const installation = await installPackedPackage('nnd-legacy-runtime-') + const consumerRequire = createRequire(join(installation.root, 'consumer.cjs')) + const WebSocket = consumerRequire('ws') + const fixture = join(installation.root, 'legacy-runtime-consumer.cjs') + await copyFile(join(FIXTURE_DIRECTORY, 'legacy-runtime-consumer.cjs'), fixture) + + const token = `${process.platform}-${process.version}-${process.pid}` + const expectedBody = Buffer.from(`legacy-pack-runtime:${token}`) + const origin = http.createServer((_request, response) => { + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': String(expectedBody.length), + 'x-nnd-pack-token': token + }) + response.end(expectedBody) + }) + const messages = [] + let child + let client + + try { + await listen(origin) + const address = origin.address() + const requestUrl = `http://127.0.0.1:${address.port}/compat?token=${encodeURIComponent(token)}` + + child = spawn(process.execPath, [fixture], { + cwd: installation.root, + env: { ...process.env }, + stdio: ['ignore', 'pipe', 'pipe', 'ipc'] + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (stdout += chunk)) + child.stderr.on('data', (chunk) => (stderr += chunk)) + child.on('message', (message) => messages.push(message)) + + const readyMessage = await waitForMessage( + child, + messages, + (message) => message?.type === 'ready' + ) + const ready = readyMessage.ready + assert.equal(ready.mode, 'legacy') + assert.match(ready.target.webSocketDebuggerUrl, /^ws:\/\/127\.0\.0\.1:\d+\//) + assert.match(ready.target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) + + const discovery = await requestBuffer(ready.target.discoveryUrl) + assert.equal(discovery.response.statusCode, 200) + const targets = JSON.parse(discovery.body.toString('utf8')) + assert.ok(targets.some((target) => target.id === ready.target.id)) + + client = await new CdpClient(WebSocket, ready.target.webSocketDebuggerUrl).connect() + assert.deepEqual(await client.command('Network.enable'), {}) + + const requestEventPromise = client.waitForEvent( + 'Network.requestWillBeSent', + (params) => params.request?.url === requestUrl + ) + const requestMessageId = randomUUID() + const resultPromise = waitForMessage( + child, + messages, + (message) => message?.type === 'request-result' && message.id === requestMessageId + ) + await send(child, { type: 'request', id: requestMessageId, url: requestUrl }) + const resultMessage = await resultPromise + assert.equal(resultMessage.error, undefined, resultMessage.error) + assert.equal(resultMessage.result.status, 200) + assert.deepEqual(Buffer.from(resultMessage.result.bodyBase64, 'base64'), expectedBody) + + const requestEvent = await requestEventPromise + const requestId = requestEvent.requestId + const responseEvent = await client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === requestId + ) + const finishedEvent = await client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ) + assert.equal(responseEvent.response.status, 200) + assert.equal(typeof finishedEvent.timestamp, 'number') + + const body = await client.command('Network.getResponseBody', { requestId }) + const observedBody = body.base64Encoded + ? Buffer.from(body.body, 'base64') + : Buffer.from(body.body, 'utf8') + assert.deepEqual(observedBody, expectedBody) + + for (const method of [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.loadingFinished' + ]) { + assert.equal( + client.events.filter( + (message) => message.method === method && message.params.requestId === requestId + ).length, + 1, + `${method} must be emitted exactly once` + ) + } + + await client.close() + client = undefined + const shutdown = waitForMessage( + child, + messages, + (message) => message?.type === 'shutdown-complete' + ) + await send(child, { type: 'shutdown' }) + await shutdown + await waitForExit(child) + assert.equal(child.exitCode, 0, `stdout:\n${stdout}\nstderr:\n${stderr}`) + + await assert.rejects( + requestBuffer(ready.target.discoveryUrl, 1_000), + 'dispose must close the Legacy discovery endpoint before resolving' + ) + + const finite = await runFinitePreload(installation.packageDirectory) + assert.equal( + finite.code, + 0, + `packed finite preload failed on ${process.version}:\n${finite.stderr}` + ) + assert.equal(finite.signal, null) + assert.doesNotMatch(finite.stderr, /NND_LEGACY_CHILD_START_FAILED/) + assert.equal(finite.stdout.match(/@@NND_PACK_FINITE@@/g)?.length, 1) + const readyPrefix = '[nnd:ready] ' + const readyLines = finite.stderr.split(/\r?\n/).filter((line) => line.includes(readyPrefix)) + assert.equal(readyLines.length, 1, finite.stderr) + const finiteReady = JSON.parse( + readyLines[0].slice(readyLines[0].indexOf(readyPrefix) + readyPrefix.length) + ) + assert.equal(finiteReady.mode, 'legacy') + assert.match(finiteReady.target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) + await waitForEndpointClosed(finiteReady.target.discoveryUrl) + } finally { + await Promise.allSettled([client?.close(), closeServer(origin)]) + if (child && child.exitCode === null && child.signalCode === null) { + child.kill('SIGTERM') + await waitForExit(child, 2_000).catch(() => child.kill('SIGKILL')) + } + await removePackedInstallation(installation) + } + } +) diff --git a/packages/network-debugger/test/e2e/pack/native-runtime-consumer.cjs b/packages/network-debugger/test/e2e/pack/native-runtime-consumer.cjs new file mode 100644 index 0000000..a1dff55 --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/native-runtime-consumer.cjs @@ -0,0 +1,87 @@ +'use strict' + +const http = require('node:http') +const https = require('node:https') +const { register } = require('node-network-devtools') + +if (!process.send) throw new Error('Native runtime consumer requires an IPC channel') + +const originals = { + fetch: globalThis.fetch, + httpRequest: http.request, + httpsRequest: https.request +} + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => (error ? reject(error) : resolve())) + }) +} + +function request(url) { + return new Promise((resolve, reject) => { + const outgoing = http.get(url, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => { + resolve({ + status: response.statusCode, + bodyBase64: Buffer.concat(chunks).toString('base64') + }) + }) + }) + outgoing.once('error', reject) + }) +} + +async function main() { + const registration = register({ + mode: 'native', + inspector: { host: '127.0.0.1', port: 0 }, + devtools: { open: false } + }) + const ready = await registration.ready + await send({ + type: 'ready', + ready, + pid: process.pid, + functionsUnchanged: { + fetch: globalThis.fetch === originals.fetch, + httpRequest: http.request === originals.httpRequest, + httpsRequest: https.request === originals.httpsRequest + } + }) + + process.on('message', async (message) => { + if (message?.type === 'request') { + try { + await send({ type: 'request-result', id: message.id, result: await request(message.url) }) + } catch (error) { + await send({ + type: 'request-result', + id: message.id, + error: error instanceof Error ? error.stack : String(error) + }) + } + return + } + + if (message?.type === 'shutdown') { + await registration.dispose() + await send({ type: 'shutdown-complete' }) + process.removeAllListeners('message') + process.disconnect() + } + }) +} + +main().catch(async (error) => { + console.error(error) + try { + await send({ type: 'fatal', error: error instanceof Error ? error.stack : String(error) }) + } finally { + process.exitCode = 1 + process.disconnect() + } +}) diff --git a/packages/network-debugger/test/e2e/pack/native-runtime.test.mjs b/packages/network-debugger/test/e2e/pack/native-runtime.test.mjs new file mode 100644 index 0000000..3f742ec --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/native-runtime.test.mjs @@ -0,0 +1,379 @@ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { copyFile } from 'node:fs/promises' +import http from 'node:http' +import { createRequire } from 'node:module' +import test from 'node:test' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { installPackedPackage, removePackedInstallation } from './packed-package.mjs' + +const TEST_TIMEOUT_MS = 120_000 +const IO_TIMEOUT_MS = 10_000 +const FIXTURE_DIRECTORY = dirname(fileURLToPath(import.meta.url)) + +class CdpClient { + constructor(WebSocket, url) { + this.WebSocket = WebSocket + this.url = url + this.nextId = 0 + this.pending = new Map() + this.events = [] + this.waiters = new Set() + } + + async connect() { + const socket = new this.WebSocket(this.url) + this.socket = socket + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + socket.terminate() + reject(new Error(`Timed out connecting to ${this.url}`)) + }, IO_TIMEOUT_MS) + const cleanup = () => { + clearTimeout(timer) + socket.off('open', onOpen) + socket.off('error', onError) + } + const onOpen = () => { + cleanup() + resolve() + } + const onError = (error) => { + cleanup() + reject(error) + } + socket.once('open', onOpen) + socket.once('error', onError) + }) + socket.on('message', (data) => this.onMessage(data)) + socket.on('error', (error) => this.onDisconnect(error)) + socket.on('close', () => this.onDisconnect(new Error('CDP WebSocket closed'))) + return this + } + + command(method, params = {}) { + const id = ++this.nextId + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id) + reject(new Error(`Timed out waiting for ${method} (${id})`)) + }, IO_TIMEOUT_MS) + this.pending.set(id, { + resolve: (message) => { + clearTimeout(timer) + if (message.error) { + reject(new Error(`${method} failed: ${message.error.message}`)) + } else { + resolve(message.result) + } + }, + reject: (error) => { + clearTimeout(timer) + reject(error) + } + }) + this.socket.send(JSON.stringify({ id, method, params }), (error) => { + if (!error) return + const pending = this.pending.get(id) + this.pending.delete(id) + pending?.reject(error) + }) + }) + } + + waitForEvent(method, predicate = () => true) { + const existing = this.events.find( + (message) => message.method === method && predicate(message.params) + ) + if (existing) return Promise.resolve(existing.params) + + return new Promise((resolve, reject) => { + const waiter = { method, predicate, resolve, reject, timer: undefined } + waiter.timer = setTimeout(() => { + this.waiters.delete(waiter) + reject(new Error(`Timed out waiting for ${method}`)) + }, IO_TIMEOUT_MS) + this.waiters.add(waiter) + }) + } + + onMessage(data) { + const message = JSON.parse(data.toString()) + if (message.id !== undefined) { + const pending = this.pending.get(message.id) + if (!pending) return + this.pending.delete(message.id) + pending.resolve(message) + return + } + if (!message.method) return + this.events.push(message) + for (const waiter of [...this.waiters]) { + if (waiter.method !== message.method || !waiter.predicate(message.params)) continue + clearTimeout(waiter.timer) + this.waiters.delete(waiter) + waiter.resolve(message.params) + } + } + + onDisconnect(error) { + for (const pending of this.pending.values()) pending.reject(error) + this.pending.clear() + for (const waiter of this.waiters) { + clearTimeout(waiter.timer) + waiter.reject(error) + } + this.waiters.clear() + } + + async close() { + const socket = this.socket + if (!socket || socket.readyState === this.WebSocket.CLOSED) return + await new Promise((resolve) => { + const timer = setTimeout(() => { + socket.terminate() + resolve() + }, 2_000) + socket.once('close', () => { + clearTimeout(timer) + resolve() + }) + socket.close() + }) + } +} + +function listen(server) { + return new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) +} + +function closeServer(server) { + if (!server?.listening) return Promise.resolve() + return new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ) +} + +function requestBuffer(url, timeoutMs = IO_TIMEOUT_MS) { + return new Promise((resolve, reject) => { + const request = http.get(url, { agent: false }, (response) => { + const chunks = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + response.once('error', reject) + response.once('end', () => resolve({ response, body: Buffer.concat(chunks) })) + }) + request.setTimeout(timeoutMs, () => request.destroy(new Error(`Timed out requesting ${url}`))) + request.once('error', reject) + }) +} + +function waitForMessage(child, messages, predicate, timeoutMs = IO_TIMEOUT_MS) { + const existing = messages.find(predicate) + if (existing) return Promise.resolve(existing) + if (child.exitCode !== null || child.signalCode !== null) { + return Promise.reject(new Error(`Native consumer already exited with ${child.exitCode}`)) + } + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup() + reject(new Error('Timed out waiting for Native consumer IPC')) + }, timeoutMs) + const cleanup = () => { + clearTimeout(timer) + child.off('message', onMessage) + child.off('exit', onExit) + } + const onMessage = (message) => { + if (!predicate(message)) return + cleanup() + resolve(message) + } + const onExit = (code, signal) => { + cleanup() + reject(new Error(`Native consumer exited before IPC (code=${code}, signal=${signal})`)) + } + child.on('message', onMessage) + child.once('exit', onExit) + }) +} + +function send(child, message) { + return new Promise((resolve, reject) => + child.send(message, (error) => (error ? reject(error) : resolve())) + ) +} + +function waitForExit(child, timeoutMs = IO_TIMEOUT_MS) { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve() + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + child.off('exit', onExit) + reject(new Error('Timed out waiting for Native consumer to exit')) + }, timeoutMs) + const onExit = () => { + clearTimeout(timer) + resolve() + } + child.once('exit', onExit) + }) +} + +test( + 'the packed Native adapter reports a real HTTP lifecycle under this Node runtime', + { timeout: TEST_TIMEOUT_MS }, + async () => { + const installation = await installPackedPackage('nnd-native-runtime-') + const consumerRequire = createRequire(join(installation.root, 'consumer.cjs')) + const WebSocket = consumerRequire('ws') + const fixture = join(installation.root, 'native-runtime-consumer.cjs') + await copyFile(join(FIXTURE_DIRECTORY, 'native-runtime-consumer.cjs'), fixture) + + const token = `${process.platform}-${process.version}-${process.pid}` + const expectedBody = Buffer.from(`native-pack-runtime:${token}`) + const origin = http.createServer((_request, response) => { + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': String(expectedBody.length), + 'x-nnd-pack-token': token + }) + response.end(expectedBody) + }) + const messages = [] + let child + let client + + try { + await listen(origin) + const address = origin.address() + const requestUrl = `http://127.0.0.1:${address.port}/compat?token=${encodeURIComponent(token)}` + + child = spawn(process.execPath, ['--experimental-network-inspection', fixture], { + cwd: installation.root, + env: { ...process.env }, + stdio: ['ignore', 'pipe', 'pipe', 'ipc'] + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => (stdout += chunk)) + child.stderr.on('data', (chunk) => (stderr += chunk)) + child.on('message', (message) => messages.push(message)) + + const readyMessage = await waitForMessage( + child, + messages, + (message) => message?.type === 'ready' + ) + const ready = readyMessage.ready + assert.equal(ready.mode, 'native') + assert.deepEqual(readyMessage.functionsUnchanged, { + fetch: true, + httpRequest: true, + httpsRequest: true + }) + assert.equal(ready.capabilities.http, true) + const nodeMajor = Number(process.versions.node.split('.')[0]) + assert.equal( + ready.capabilities.responseBody, + nodeMajor >= 24, + 'Node 22 must not advertise cross-transport response bodies; Node 24+ must' + ) + assert.match(ready.target.webSocketDebuggerUrl, /^ws:\/\/127\.0\.0\.1:\d+\//) + assert.match(ready.target.discoveryUrl, /^http:\/\/127\.0\.0\.1:\d+\/json\/list$/) + + const discovery = await requestBuffer(ready.target.discoveryUrl) + assert.equal(discovery.response.statusCode, 200) + const targets = JSON.parse(discovery.body.toString('utf8')) + assert.ok(targets.some((target) => target.id === ready.target.id)) + + client = await new CdpClient(WebSocket, ready.target.webSocketDebuggerUrl).connect() + assert.deepEqual(await client.command('Network.enable'), {}) + + const requestEventPromise = client.waitForEvent( + 'Network.requestWillBeSent', + (params) => params.request?.url === requestUrl + ) + const requestMessageId = randomUUID() + const resultPromise = waitForMessage( + child, + messages, + (message) => message?.type === 'request-result' && message.id === requestMessageId + ) + await send(child, { type: 'request', id: requestMessageId, url: requestUrl }) + const resultMessage = await resultPromise + assert.equal(resultMessage.error, undefined, resultMessage.error) + assert.equal(resultMessage.result.status, 200) + assert.deepEqual(Buffer.from(resultMessage.result.bodyBase64, 'base64'), expectedBody) + + const requestEvent = await requestEventPromise + const requestId = requestEvent.requestId + const responseEvent = await client.waitForEvent( + 'Network.responseReceived', + (params) => params.requestId === requestId + ) + const finishedEvent = await client.waitForEvent( + 'Network.loadingFinished', + (params) => params.requestId === requestId + ) + assert.equal(responseEvent.response.status, 200) + assert.equal(typeof finishedEvent.timestamp, 'number') + + if (ready.capabilities.responseBody) { + const body = await client.command('Network.getResponseBody', { requestId }) + const observedBody = body.base64Encoded + ? Buffer.from(body.body, 'base64') + : Buffer.from(body.body, 'utf8') + assert.deepEqual(observedBody, expectedBody) + } + + for (const method of [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + 'Network.loadingFinished' + ]) { + assert.equal( + client.events.filter( + (message) => message.method === method && message.params.requestId === requestId + ).length, + 1, + `${method} must be emitted exactly once` + ) + } + + await client.close() + client = undefined + const shutdown = waitForMessage( + child, + messages, + (message) => message?.type === 'shutdown-complete' + ) + await send(child, { type: 'shutdown' }) + await shutdown + await waitForExit(child) + assert.equal(child.exitCode, 0, `stdout:\n${stdout}\nstderr:\n${stderr}`) + + await assert.rejects( + requestBuffer(ready.target.discoveryUrl, 1_000), + 'dispose must close the Native discovery endpoint before resolving' + ) + } finally { + await Promise.allSettled([client?.close(), closeServer(origin)]) + if (child && child.exitCode === null && child.signalCode === null) { + child.kill('SIGTERM') + await waitForExit(child, 2_000).catch(() => child.kill('SIGKILL')) + } + await removePackedInstallation(installation) + } + } +) diff --git a/packages/network-debugger/test/e2e/pack/pack-consumer.test.mjs b/packages/network-debugger/test/e2e/pack/pack-consumer.test.mjs new file mode 100644 index 0000000..17b5bc2 --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/pack-consumer.test.mjs @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict' +import { access, readFile, writeFile } from 'node:fs/promises' +import test from 'node:test' +import { join, normalize } from 'node:path' + +import { installPackedPackage, removePackedInstallation, runNode } from './packed-package.mjs' + +function exportedFiles(exports, files = []) { + if (typeof exports === 'string') { + if (exports.startsWith('./')) files.push(exports) + return files + } + if (!exports || typeof exports !== 'object') return files + for (const value of Object.values(exports)) exportedFiles(value, files) + return files +} + +test( + 'the exact npm tarball has complete exports and resolves from CJS and ESM consumers', + { timeout: 120_000 }, + async (t) => { + const installation = await installPackedPackage() + t.after(() => removePackedInstallation(installation)) + + const { root, packageDirectory, manifest } = installation + const publicTargets = new Set([ + ...exportedFiles(manifest.exports), + ...Object.values(manifest.bin ?? {}) + ]) + assert.ok(publicTargets.size > 0, 'package.json must declare public exports or bins') + + for (const relativeTarget of publicTargets) { + assert.ok( + typeof relativeTarget === 'string' && relativeTarget.startsWith('./'), + `public target must be package-relative: ${relativeTarget}` + ) + const normalizedTarget = normalize(relativeTarget).replace(/^(\.\.[/\\])+/, '') + assert.equal( + normalizedTarget, + normalize(relativeTarget), + `public target must not escape the package: ${relativeTarget}` + ) + await access(join(packageDirectory, normalizedTarget)).catch(() => { + assert.fail(`packed package is missing public target ${relativeTarget}`) + }) + } + + const installedManifest = JSON.parse( + await readFile(join(packageDirectory, 'package.json'), 'utf8') + ) + assert.equal(installedManifest.name, 'node-network-devtools') + assert.match(installedManifest.version, /^\d+\.\d+\.\d+(?:[-+].+)?$/) + assert.equal(installedManifest.engines?.node, '>=18.18') + assert.equal(installedManifest.exports?.['./register']?.types, './dist/preload/register.d.ts') + assert.equal(installedManifest.exports?.['./preload']?.types, './dist/preload/register.d.ts') + + const preloadDeclaration = await readFile( + join(packageDirectory, 'dist/preload/register.d.ts'), + 'utf8' + ) + assert.match(preloadDeclaration, /^export \{\};/) + assert.doesNotMatch( + preloadDeclaration, + /declare (?:function|const) (?:preload|getPreloadHandle)/ + ) + + const license = await readFile(join(packageDirectory, 'LICENSE'), 'utf8') + assert.match(license, /^MIT License/) + assert.match(license, /included in all\s+copies or substantial portions/) + + const commonJsEntry = join(root, 'consumer.cjs') + await writeFile( + commonJsEntry, + `'use strict'\n` + + `const assert = require('node:assert/strict')\n` + + `const api = require('node-network-devtools')\n` + + `const config = require('node-network-devtools/config')\n` + + `assert.equal(typeof api.register, 'function')\n` + + `assert.equal(typeof config.defineConfig, 'function')\n` + + `assert.equal(config.defineConfig({ mode: 'legacy' }).mode, 'legacy')\n` + + `process.stdout.write('cjs-consumer-ok\\n')\n`, + 'utf8' + ) + const cjs = await runNode(commonJsEntry, { cwd: root }) + assert.match(cjs.stdout, /cjs-consumer-ok/) + + const esmEntry = join(root, 'consumer.mjs') + await writeFile( + esmEntry, + `import assert from 'node:assert/strict'\n` + + `import { register } from 'node-network-devtools'\n` + + `import { defineConfig } from 'node-network-devtools/config'\n` + + `assert.equal(typeof register, 'function')\n` + + `assert.equal(typeof defineConfig, 'function')\n` + + `assert.equal(defineConfig({ mode: 'legacy' }).mode, 'legacy')\n` + + `process.stdout.write('esm-consumer-ok\\n')\n`, + 'utf8' + ) + const esm = await runNode(esmEntry, { cwd: root }) + assert.match(esm.stdout, /esm-consumer-ok/) + } +) diff --git a/packages/network-debugger/test/e2e/pack/packed-package.mjs b/packages/network-debugger/test/e2e/pack/packed-package.mjs new file mode 100644 index 0000000..d3a5146 --- /dev/null +++ b/packages/network-debugger/test/e2e/pack/packed-package.mjs @@ -0,0 +1,105 @@ +import { execFile } from 'node:child_process' +import { mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join, resolve } from 'node:path' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const PACKAGE_NAME = 'node-network-devtools' + +function npmExecutable() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm' +} + +async function assertTarball(path) { + if (!path.endsWith('.tgz')) { + throw new Error(`Expected an npm .tgz, received: ${path}`) + } + return realpath(path) +} + +/** + * Resolve the one package artifact downloaded by CI. Tests deliberately do not + * fall back to `npm pack` or a repository dist directory: the exact uploaded + * tarball is the release candidate under test. + */ +export async function locatePackedTarball() { + if (process.env.NND_PACK_TARBALL) { + return assertTarball(resolve(process.env.NND_PACK_TARBALL)) + } + + const directory = process.env.NND_PACK_DIR + if (!directory) { + throw new Error('Set NND_PACK_TARBALL or NND_PACK_DIR to the downloaded npm artifact.') + } + + const entries = (await readdir(resolve(directory))).filter((entry) => entry.endsWith('.tgz')) + if (entries.length !== 1) { + throw new Error( + `Expected exactly one .tgz in ${directory}; found ${entries.length}: ${entries.join(', ')}` + ) + } + return assertTarball(join(resolve(directory), entries[0])) +} + +export async function installPackedPackage(prefix = 'node-network-devtools-pack-') { + const tarball = await locatePackedTarball() + const root = await mkdtemp(join(tmpdir(), prefix)) + await writeFile( + join(root, 'package.json'), + JSON.stringify({ name: 'nnd-packed-consumer', version: '0.0.0', private: true }, null, 2), + 'utf8' + ) + + try { + await execFileAsync( + npmExecutable(), + ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock', tarball], + { + cwd: root, + env: { ...process.env, npm_config_update_notifier: 'false' }, + // Windows npm is a .cmd shim and therefore needs cmd.exe; POSIX keeps + // the safer direct execFile path. + shell: process.platform === 'win32', + maxBuffer: 10 * 1024 * 1024 + } + ) + } catch (error) { + await rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }) + const output = [error?.stdout, error?.stderr].filter(Boolean).join('\n') + throw new Error(`Could not install ${basename(tarball)} with ${npmExecutable()}:\n${output}`, { + cause: error + }) + } + + const packageDirectory = join(root, 'node_modules', PACKAGE_NAME) + const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8')) + if (manifest.name !== PACKAGE_NAME) { + throw new Error(`Installed unexpected package ${manifest.name ?? '(missing name)'}`) + } + + return { root, packageDirectory, manifest, tarball } +} + +export async function removePackedInstallation(installation) { + if (!installation?.root) return + await rm(installation.root, { + recursive: true, + force: true, + maxRetries: process.platform === 'win32' ? 10 : 3, + retryDelay: 100 + }) +} + +export async function runNode(entry, { cwd, env = {}, args = [] } = {}) { + try { + return await execFileAsync(process.execPath, [...args, entry], { + cwd, + env: { ...process.env, ...env }, + maxBuffer: 10 * 1024 * 1024 + }) + } catch (error) { + const output = [error?.stdout, error?.stderr].filter(Boolean).join('\n') + throw new Error(`Packed consumer ${entry} failed:\n${output}`, { cause: error }) + } +} diff --git a/packages/network-debugger/vite.config.ts b/packages/network-debugger/vite.config.ts index 2161f5a..c63659a 100644 --- a/packages/network-debugger/vite.config.ts +++ b/packages/network-debugger/vite.config.ts @@ -9,7 +9,12 @@ export default defineConfig(({ mode }) => ({ sourcemap: mode === 'development', outDir: 'dist', lib: { - entry: [resolve(__dirname, 'src/index.ts'), resolve(__dirname, 'src/fork/fork.ts')] + entry: { + index: resolve(__dirname, 'src/index.ts'), + fork: resolve(__dirname, 'src/fork/fork.ts'), + cli: resolve(__dirname, 'src/cli/bin.ts'), + config: resolve(__dirname, 'src/config/index.ts') + } }, rollupOptions: { external: [ @@ -24,16 +29,23 @@ export default defineConfig(({ mode }) => ({ 'path', 'url', 'node:zlib', + 'node:fs', + 'node:fs/promises', + 'node:module', + 'node:path', + 'node:child_process', 'node:assert', 'node:util', 'node:events', 'node:url', 'node:net', 'node:http', + 'node:https', 'node:tls', 'node:stream', 'node:querystring', 'node:crypto', + 'node:v8', 'node:diagnostics_channel', 'node:async_hooks', 'node:buffer', @@ -70,6 +82,7 @@ export default defineConfig(({ mode }) => ({ 'node:stream': 'node_stream', 'node:querystring': 'node_querystring', 'node:crypto': 'node_crypto', + 'node:v8': 'node_v8', 'node:inspector': 'node_inspector', bufferutil: 'bufferutil' } diff --git a/packages/network-debugger/vite.preload.config.ts b/packages/network-debugger/vite.preload.config.ts new file mode 100644 index 0000000..f7aa4ca --- /dev/null +++ b/packages/network-debugger/vite.preload.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vite' +import { resolve } from 'path' + +export default defineConfig({ + build: { + target: 'es2022', + outDir: 'dist', + emptyOutDir: false, + lib: { + entry: resolve(__dirname, 'src/preload/register.ts'), + formats: ['es'], + fileName: () => 'register.mjs' + }, + rollupOptions: { + external: [ + 'http', + 'https', + 'child_process', + 'open', + 'ws', + 'iconv-lite', + 'zlib', + 'fs', + 'path', + 'url', + 'stream', + 'net', + 'undici', + 'bufferutil', + /^node:/ + ] + } + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a0832c..c0e7a58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: devDependencies: '@commitlint/config-conventional': @@ -259,14 +258,14 @@ importers: dependencies: vuepress-theme-hope: specifier: 2.0.0-rc.62 - version: 2.0.0-rc.62(@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))))(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + version: 2.0.0-rc.62(@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))))(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) devDependencies: '@vuepress/bundler-vite': specifier: 2.0.0-rc.18 - version: 2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1) + version: 2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1) '@vuepress/theme-default': specifier: 2.0.0-rc.65 - version: 2.0.0-rc.65(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + version: 2.0.0-rc.65(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) sass-embedded: specifier: ^1.82.0 version: 1.82.0 @@ -275,7 +274,7 @@ importers: version: 3.5.12(typescript@5.6.3) vuepress: specifier: 2.0.0-rc.18 - version: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + version: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) packages/eslint-config: devDependencies: @@ -309,9 +308,6 @@ importers: iconv-lite: specifier: ^0.7.0 version: 0.7.2 - inspector: - specifier: ^0.5.0 - version: 0.5.0 open: specifier: ^8.4.2 version: 8.4.2 @@ -322,6 +318,9 @@ importers: specifier: ^8.17.1 version: 8.20.0(bufferutil@4.0.9) devDependencies: + '@playwright/test': + specifier: 1.62.1 + version: 1.62.1 '@types/node': specifier: ^20.11.0 version: 20.16.11 @@ -336,7 +335,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.6.3) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1)) + version: 4.0.18(vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1)) envinfo: specifier: ^7.14.0 version: 7.14.0 @@ -349,6 +348,9 @@ importers: tslib: specifier: 2.6.2 version: 2.6.2 + tsx: + specifier: 4.23.12 + version: 4.23.12 vite: specifier: ^5.2.10 version: 5.4.9(@types/node@20.16.11)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1) @@ -357,17 +359,22 @@ importers: version: 3.9.1(@types/node@20.16.11)(rollup@4.57.0)(typescript@5.6.3)(vite@5.4.9(@types/node@20.16.11)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1) + version: 4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1) packages: - '@ampproject/remapping@2.2.1': - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + } + engines: { node: '>=6.0.0' } '@angular-devkit/core@16.0.1': - resolution: {integrity: sha512-2uz98IqkKJlgnHbWQ7VeL4pb+snGAZXIama2KXi+k9GsRntdcw+udX8rL3G9SdUGUF+m6+147Y1oRBMHsO/v4w==} - engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + resolution: + { + integrity: sha512-2uz98IqkKJlgnHbWQ7VeL4pb+snGAZXIama2KXi+k9GsRntdcw+udX8rL3G9SdUGUF+m6+147Y1oRBMHsO/v4w== + } + engines: { node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0' } peerDependencies: chokidar: ^3.5.2 peerDependenciesMeta: @@ -375,637 +382,1249 @@ packages: optional: true '@angular-devkit/schematics-cli@16.0.1': - resolution: {integrity: sha512-6KLA125dpgd6oJGtiO2JpZAb92uOG3njQGIt7NFcuQGW/5GO7J41vMXH9cBAfdtbV8SIggSmR/cIEE9ijfj6YQ==} - engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + resolution: + { + integrity: sha512-6KLA125dpgd6oJGtiO2JpZAb92uOG3njQGIt7NFcuQGW/5GO7J41vMXH9cBAfdtbV8SIggSmR/cIEE9ijfj6YQ== + } + engines: { node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0' } hasBin: true '@angular-devkit/schematics@16.0.1': - resolution: {integrity: sha512-A9D0LTYmiqiBa90GKcSuWb7hUouGIbm/AHbJbjL85WLLRbQA2PwKl7P5Mpd6nS/ZC0kfG4VQY3VOaDvb3qpI9g==} - engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + resolution: + { + integrity: sha512-A9D0LTYmiqiBa90GKcSuWb7hUouGIbm/AHbJbjL85WLLRbQA2PwKl7P5Mpd6nS/ZC0kfG4VQY3VOaDvb3qpI9g== + } + engines: { node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0' } '@babel/code-frame@7.25.7': - resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== + } + engines: { node: '>=6.9.0' } '@babel/compat-data@7.25.8': - resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== + } + engines: { node: '>=6.9.0' } '@babel/core@7.25.8': - resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg== + } + engines: { node: '>=6.9.0' } '@babel/eslint-parser@7.25.8': - resolution: {integrity: sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + resolution: + { + integrity: sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg== + } + engines: { node: ^10.13.0 || ^12.13.0 || >=14.0.0 } peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 '@babel/generator@7.25.7': - resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== + } + engines: { node: '>=6.9.0' } '@babel/helper-compilation-targets@7.25.7': - resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== + } + engines: { node: '>=6.9.0' } '@babel/helper-module-imports@7.25.7': - resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw== + } + engines: { node: '>=6.9.0' } '@babel/helper-module-transforms@7.25.7': - resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-plugin-utils@7.25.7': - resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== + } + engines: { node: '>=6.9.0' } '@babel/helper-simple-access@7.25.7': - resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ== + } + engines: { node: '>=6.9.0' } '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== + } + engines: { node: '>=6.9.0' } '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + } + engines: { node: '>=6.9.0' } '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== + } + engines: { node: '>=6.9.0' } '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + } + engines: { node: '>=6.9.0' } '@babel/helper-validator-option@7.25.7': - resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== + } + engines: { node: '>=6.9.0' } '@babel/helpers@7.25.7': - resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA== + } + engines: { node: '>=6.9.0' } '@babel/highlight@7.25.7': - resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== + } + engines: { node: '>=6.9.0' } '@babel/parser@7.25.8': - resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ== + } + engines: { node: '>=6.0.0' } hasBin: true '@babel/parser@7.28.6': - resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + } + engines: { node: '>=6.0.0' } hasBin: true '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + resolution: + { + integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + resolution: + { + integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + resolution: + { + integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-attributes@7.25.7': - resolution: {integrity: sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + resolution: + { + integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + resolution: + { + integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-jsx@7.25.7': - resolution: {integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + resolution: + { + integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + resolution: + { + integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + resolution: + { + integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + resolution: + { + integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + resolution: + { + integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + resolution: + { + integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-typescript@7.25.7': - resolution: {integrity: sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g== + } + engines: { node: '>=6.9.0' } peerDependencies: '@babel/core': ^7.0.0-0 '@babel/template@7.25.7': - resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA== + } + engines: { node: '>=6.9.0' } '@babel/traverse@7.25.7': - resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== + } + engines: { node: '>=6.9.0' } '@babel/types@7.25.8': - resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== + } + engines: { node: '>=6.9.0' } '@babel/types@7.28.6': - resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + } + engines: { node: '>=6.9.0' } '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + resolution: + { + integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + } '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== + } + engines: { node: '>=18' } '@borewit/text-codec@0.2.1': - resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + resolution: + { + integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw== + } '@bufbuild/protobuf@2.2.2': - resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==} + resolution: + { + integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A== + } '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} + resolution: + { + integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + } + engines: { node: '>=0.1.90' } '@commitlint/cli@19.5.0': - resolution: {integrity: sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ== + } + engines: { node: '>=v18' } hasBin: true '@commitlint/config-conventional@19.5.0': - resolution: {integrity: sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg== + } + engines: { node: '>=v18' } '@commitlint/config-validator@19.5.0': - resolution: {integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw== + } + engines: { node: '>=v18' } '@commitlint/ensure@19.5.0': - resolution: {integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg== + } + engines: { node: '>=v18' } '@commitlint/execute-rule@19.5.0': - resolution: {integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg== + } + engines: { node: '>=v18' } '@commitlint/format@19.5.0': - resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A== + } + engines: { node: '>=v18' } '@commitlint/is-ignored@19.5.0': - resolution: {integrity: sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w== + } + engines: { node: '>=v18' } '@commitlint/lint@19.5.0': - resolution: {integrity: sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg== + } + engines: { node: '>=v18' } '@commitlint/load@19.5.0': - resolution: {integrity: sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA== + } + engines: { node: '>=v18' } '@commitlint/message@19.5.0': - resolution: {integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ== + } + engines: { node: '>=v18' } '@commitlint/parse@19.5.0': - resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw== + } + engines: { node: '>=v18' } '@commitlint/read@19.5.0': - resolution: {integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ== + } + engines: { node: '>=v18' } '@commitlint/resolve-extends@19.5.0': - resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA== + } + engines: { node: '>=v18' } '@commitlint/rules@19.5.0': - resolution: {integrity: sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw== + } + engines: { node: '>=v18' } '@commitlint/to-lines@19.5.0': - resolution: {integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ== + } + engines: { node: '>=v18' } '@commitlint/top-level@19.5.0': - resolution: {integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng== + } + engines: { node: '>=v18' } '@commitlint/types@19.5.0': - resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg== + } + engines: { node: '>=v18' } '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + } + engines: { node: '>=12' } '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + } + engines: { node: '>=12' } cpu: [ppc64] os: [aix] '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== + } + engines: { node: '>=18' } + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: + { + integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + } + engines: { node: '>=18' } cpu: [ppc64] os: [aix] '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + } + engines: { node: '>=12' } cpu: [arm64] os: [android] '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: + { + integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + } + engines: { node: '>=18' } cpu: [arm64] os: [android] '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + } + engines: { node: '>=12' } cpu: [arm] os: [android] '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== + } + engines: { node: '>=18' } + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: + { + integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + } + engines: { node: '>=18' } cpu: [arm] os: [android] '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + } + engines: { node: '>=12' } cpu: [x64] os: [android] '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== + } + engines: { node: '>=18' } + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: + { + integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + } + engines: { node: '>=18' } cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + } + engines: { node: '>=12' } cpu: [arm64] os: [darwin] '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: + { + integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + } + engines: { node: '>=18' } cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + } + engines: { node: '>=12' } cpu: [x64] os: [darwin] '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== + } + engines: { node: '>=18' } + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: + { + integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + } + engines: { node: '>=18' } cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + } + engines: { node: '>=12' } cpu: [arm64] os: [freebsd] '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: + { + integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + } + engines: { node: '>=18' } cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + } + engines: { node: '>=12' } cpu: [x64] os: [freebsd] '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== + } + engines: { node: '>=18' } + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: + { + integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + } + engines: { node: '>=18' } cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + } + engines: { node: '>=12' } cpu: [arm64] os: [linux] '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: + { + integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + } + engines: { node: '>=18' } cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + } + engines: { node: '>=12' } cpu: [arm] os: [linux] '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== + } + engines: { node: '>=18' } + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: + { + integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + } + engines: { node: '>=18' } cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + } + engines: { node: '>=12' } cpu: [ia32] os: [linux] '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== + } + engines: { node: '>=18' } + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: + { + integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + } + engines: { node: '>=18' } cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + } + engines: { node: '>=12' } cpu: [loong64] os: [linux] '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== + } + engines: { node: '>=18' } + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: + { + integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + } + engines: { node: '>=18' } cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + } + engines: { node: '>=12' } cpu: [mips64el] os: [linux] '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== + } + engines: { node: '>=18' } + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: + { + integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + } + engines: { node: '>=18' } cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + } + engines: { node: '>=12' } cpu: [ppc64] os: [linux] '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== + } + engines: { node: '>=18' } + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: + { + integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + } + engines: { node: '>=18' } cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + } + engines: { node: '>=12' } cpu: [riscv64] os: [linux] '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== + } + engines: { node: '>=18' } + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: + { + integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + } + engines: { node: '>=18' } cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + } + engines: { node: '>=12' } cpu: [s390x] os: [linux] '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== + } + engines: { node: '>=18' } + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: + { + integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + } + engines: { node: '>=18' } cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + } + engines: { node: '>=12' } cpu: [x64] os: [linux] '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== + } + engines: { node: '>=18' } + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: + { + integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + } + engines: { node: '>=18' } cpu: [x64] os: [linux] '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: + { + integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + } + engines: { node: '>=18' } cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + } + engines: { node: '>=12' } cpu: [x64] os: [netbsd] '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== + } + engines: { node: '>=18' } + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: + { + integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + } + engines: { node: '>=18' } cpu: [x64] os: [netbsd] '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: + { + integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + } + engines: { node: '>=18' } cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + } + engines: { node: '>=12' } cpu: [x64] os: [openbsd] '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== + } + engines: { node: '>=18' } + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: + { + integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + } + engines: { node: '>=18' } cpu: [x64] os: [openbsd] '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: + { + integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + } + engines: { node: '>=18' } cpu: [arm64] os: [openharmony] '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + } + engines: { node: '>=12' } cpu: [x64] os: [sunos] '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== + } + engines: { node: '>=18' } + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: + { + integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + } + engines: { node: '>=18' } cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + } + engines: { node: '>=12' } cpu: [arm64] os: [win32] '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== + } + engines: { node: '>=18' } + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: + { + integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + } + engines: { node: '>=18' } cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + } + engines: { node: '>=12' } cpu: [ia32] os: [win32] '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== + } + engines: { node: '>=18' } + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: + { + integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + } + engines: { node: '>=18' } cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + } + engines: { node: '>=12' } cpu: [x64] os: [win32] '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== + } + engines: { node: '>=18' } + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: + { + integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + } + engines: { node: '>=18' } cpu: [x64] os: [win32] '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + resolution: + { + integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } '@hapi/bourne@3.0.0': - resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + resolution: + { + integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w== + } '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} + resolution: + { + integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + } + engines: { node: '>=10.10.0' } deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + } + engines: { node: '>=12.22' } '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + resolution: + { + integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + } deprecated: Use @eslint/object-schema instead '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + } + engines: { node: '>=8' } '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + } + engines: { node: '>=8' } '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -1013,28 +1632,46 @@ packages: optional: true '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -1042,216 +1679,357 @@ packages: optional: true '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + } + engines: { node: '>=6.0.0' } '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + } + engines: { node: '>=6.0.0' } '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + } + engines: { node: '>=6.0.0' } '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + resolution: + { + integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + } '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + resolution: + { + integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + } '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + } '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + resolution: + { + integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + } '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + } '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + resolution: + { + integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + } '@jsonjoy.com/base64@1.1.2': - resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/base64@17.65.0': - resolution: {integrity: sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/buffers@1.2.1': - resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/buffers@17.65.0': - resolution: {integrity: sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/codegen@1.0.0': - resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/codegen@17.65.0': - resolution: {integrity: sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-core@4.56.10': - resolution: {integrity: sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-fsa@4.56.10': - resolution: {integrity: sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-node-builtins@4.56.10': - resolution: {integrity: sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-node-to-fsa@4.56.10': - resolution: {integrity: sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-node-utils@4.56.10': - resolution: {integrity: sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-node@4.56.10': - resolution: {integrity: sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-print@4.56.10': - resolution: {integrity: sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/fs-snapshot@4.56.10': - resolution: {integrity: sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/json-pack@1.21.0': - resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/json-pack@17.65.0': - resolution: {integrity: sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/json-pointer@1.0.2': - resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/json-pointer@17.65.0': - resolution: {integrity: sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/util@1.9.0': - resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@jsonjoy.com/util@17.65.0': - resolution: {integrity: sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' '@lit-labs/ssr-dom-shim@1.2.1': - resolution: {integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==} + resolution: + { + integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ== + } '@lit/reactive-element@2.0.4': - resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} + resolution: + { + integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ== + } '@lukeed/csprng@1.1.0': - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== + } + engines: { node: '>=8' } '@mdit-vue/plugin-component@2.1.3': - resolution: {integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA==} + resolution: + { + integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA== + } '@mdit-vue/plugin-frontmatter@2.1.3': - resolution: {integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ==} + resolution: + { + integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ== + } '@mdit-vue/plugin-headers@2.1.3': - resolution: {integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA==} + resolution: + { + integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA== + } '@mdit-vue/plugin-sfc@2.1.3': - resolution: {integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w==} + resolution: + { + integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w== + } '@mdit-vue/plugin-title@2.1.3': - resolution: {integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw==} + resolution: + { + integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw== + } '@mdit-vue/plugin-toc@2.1.3': - resolution: {integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ==} + resolution: + { + integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ== + } '@mdit-vue/shared@2.1.3': - resolution: {integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ==} + resolution: + { + integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ== + } '@mdit-vue/types@2.1.0': - resolution: {integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA==} + resolution: + { + integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA== + } '@mdit/helper@0.14.0': - resolution: {integrity: sha512-PNWzB2ZomBfUMF8skqzxbwGvcF5Q6+jkS6iis2nvLDobwV8hMzSWD49Jf++50i1XjHwcrFm0VsHqzsVCSoVAGA==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-PNWzB2ZomBfUMF8skqzxbwGvcF5Q6+jkS6iis2nvLDobwV8hMzSWD49Jf++50i1XjHwcrFm0VsHqzsVCSoVAGA== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1259,7 +2037,10 @@ packages: optional: true '@mdit/plugin-alert@0.14.0': - resolution: {integrity: sha512-a9/E6AZkCdepiseaS7VzjTPzlDVEpDXbgfspT1V0LVJ0xHiOlPNaUhB4vuRavETFyFmbmx8Lq8SvjvN6VdIz7Q==} + resolution: + { + integrity: sha512-a9/E6AZkCdepiseaS7VzjTPzlDVEpDXbgfspT1V0LVJ0xHiOlPNaUhB4vuRavETFyFmbmx8Lq8SvjvN6VdIz7Q== + } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1267,8 +2048,11 @@ packages: optional: true '@mdit/plugin-align@0.14.0': - resolution: {integrity: sha512-lm7epUka2zMHh6DpU1juX7eBAZHaGk+gcMNPknLb9LnP+zgKqEQ6Dl/86z24N9h9biAnp4AoY1uFMQB5mMvPHw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-lm7epUka2zMHh6DpU1juX7eBAZHaGk+gcMNPknLb9LnP+zgKqEQ6Dl/86z24N9h9biAnp4AoY1uFMQB5mMvPHw== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1276,8 +2060,11 @@ packages: optional: true '@mdit/plugin-attrs@0.14.0': - resolution: {integrity: sha512-/SK/mDXF+Zly7HN3ps8RB83CIWKv8r9FPGxmq9DknpXhFFOMlE5UIP9l7E0ysWmlKElfoEq9hYgUY9Ieerphtg==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-/SK/mDXF+Zly7HN3ps8RB83CIWKv8r9FPGxmq9DknpXhFFOMlE5UIP9l7E0ysWmlKElfoEq9hYgUY9Ieerphtg== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1285,8 +2072,11 @@ packages: optional: true '@mdit/plugin-container@0.14.0': - resolution: {integrity: sha512-sYjR9GPPkdItjGXw2m4f2iKAvKK+9egq/3wnzNnsouK1Hz0Qz8rQM1VELQLBK16PJwqStGNfTQC31BeM7gVmIg==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-sYjR9GPPkdItjGXw2m4f2iKAvKK+9egq/3wnzNnsouK1Hz0Qz8rQM1VELQLBK16PJwqStGNfTQC31BeM7gVmIg== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1294,7 +2084,10 @@ packages: optional: true '@mdit/plugin-demo@0.14.0': - resolution: {integrity: sha512-pp5XTESSmvUKpIJMyM1RFCddXjTXhgw8HKDPAMmIc2BpeBzFUBgNBA3JBULE8KiZVPJBdc2HuswQFOYwb6IQ/A==} + resolution: + { + integrity: sha512-pp5XTESSmvUKpIJMyM1RFCddXjTXhgw8HKDPAMmIc2BpeBzFUBgNBA3JBULE8KiZVPJBdc2HuswQFOYwb6IQ/A== + } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1302,8 +2095,11 @@ packages: optional: true '@mdit/plugin-figure@0.14.0': - resolution: {integrity: sha512-3/gyY1XUw5A+ZP/p6t/iQ36hmvHuAwQRBjFHxwEsFVVI7OLfoqaCEEkg/VAUeyNc4DN8vPhueZrbam5EIrBtPA==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-3/gyY1XUw5A+ZP/p6t/iQ36hmvHuAwQRBjFHxwEsFVVI7OLfoqaCEEkg/VAUeyNc4DN8vPhueZrbam5EIrBtPA== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1311,14 +2107,20 @@ packages: optional: true '@mdit/plugin-footnote@0.14.0': - resolution: {integrity: sha512-GaBn/j7TiC3tFwR0iRKyo133FvJtPQ1qo0K3NGKULWD8M4i1K2tEuA6qRKELh3DlsNKxK6G81KnQTl/87gR3rw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-GaBn/j7TiC3tFwR0iRKyo133FvJtPQ1qo0K3NGKULWD8M4i1K2tEuA6qRKELh3DlsNKxK6G81KnQTl/87gR3rw== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 '@mdit/plugin-img-lazyload@0.14.0': - resolution: {integrity: sha512-4ZMKCb0YHlGzdcFGVypFecsn+sA+Fk+mdZ76Gj0OPoRHuwLSmyEN8Y6B3lvZWy1rm4p/Ea71g1nEqPs54qW+yg==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-4ZMKCb0YHlGzdcFGVypFecsn+sA+Fk+mdZ76Gj0OPoRHuwLSmyEN8Y6B3lvZWy1rm4p/Ea71g1nEqPs54qW+yg== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1326,8 +2128,11 @@ packages: optional: true '@mdit/plugin-img-mark@0.14.0': - resolution: {integrity: sha512-wVAtRby7/4/EM+6ipNLqxUo4HGN8QWxBpc8An3QdOgqT6bJsmDiTM461gZIwtbzhBdh9RE9Bg1I5BmedXjTkZQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-wVAtRby7/4/EM+6ipNLqxUo4HGN8QWxBpc8An3QdOgqT6bJsmDiTM461gZIwtbzhBdh9RE9Bg1I5BmedXjTkZQ== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1335,8 +2140,11 @@ packages: optional: true '@mdit/plugin-img-size@0.14.0': - resolution: {integrity: sha512-A+HyWSSTXW318Ly48xOf4XnKW4xTstXewoY87cSXjw0eGO/rR50n4golihEJ9ixxUlJ/E8zpGCu454cOOBriKw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-A+HyWSSTXW318Ly48xOf4XnKW4xTstXewoY87cSXjw0eGO/rR50n4golihEJ9ixxUlJ/E8zpGCu454cOOBriKw== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1344,7 +2152,10 @@ packages: optional: true '@mdit/plugin-include@0.14.0': - resolution: {integrity: sha512-Y4XXGz9lhdSEctqMdmrN4R4nJPfgPSrtTwyptiN5ArT8fx+UIJId8AYBk5ybMZBAYne2QBHI/8+2O0xrUxJzYw==} + resolution: + { + integrity: sha512-Y4XXGz9lhdSEctqMdmrN4R4nJPfgPSrtTwyptiN5ArT8fx+UIJId8AYBk5ybMZBAYne2QBHI/8+2O0xrUxJzYw== + } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1352,8 +2163,11 @@ packages: optional: true '@mdit/plugin-katex-slim@0.14.0': - resolution: {integrity: sha512-GztODGcbbd+Glmwrin8yeR6KDESyFwxKkwe1u40XQ4vAImtOswQbAk5suoFGySiuONzGiu/4RLOKnYv5Unq3jQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-GztODGcbbd+Glmwrin8yeR6KDESyFwxKkwe1u40XQ4vAImtOswQbAk5suoFGySiuONzGiu/4RLOKnYv5Unq3jQ== + } + engines: { node: '>= 18' } peerDependencies: katex: ^0.16.9 markdown-it: ^14.1.0 @@ -1364,8 +2178,11 @@ packages: optional: true '@mdit/plugin-mark@0.14.0': - resolution: {integrity: sha512-NKJB92pJissct4HCDv+GK8dMF/viX5fzf6zrO9zBmgFRfdeo6CqjuVWh+Se/mPtqwhk1f2EX80YlOXorWNjXKQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-NKJB92pJissct4HCDv+GK8dMF/viX5fzf6zrO9zBmgFRfdeo6CqjuVWh+Se/mPtqwhk1f2EX80YlOXorWNjXKQ== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1373,8 +2190,11 @@ packages: optional: true '@mdit/plugin-mathjax-slim@0.14.0': - resolution: {integrity: sha512-AOFHQ5wN8IDV8d96MH3w9irHNguLtWbhYajQcc+O5wGeWxDsd5KMAd4StgERETxo6XgLxr0NRC8HzxSMPrC2aw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-AOFHQ5wN8IDV8d96MH3w9irHNguLtWbhYajQcc+O5wGeWxDsd5KMAd4StgERETxo6XgLxr0NRC8HzxSMPrC2aw== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 mathjax-full: ^3.2.2 @@ -1385,7 +2205,10 @@ packages: optional: true '@mdit/plugin-plantuml@0.14.0': - resolution: {integrity: sha512-AZy/o6Uvjd46GWoisYLQBL37ET0T+XscI+E8nUuZGu/ihAhoZU3jxeE97WFPwBa5T3hrNd9L1XfLXTTDM2ZQWw==} + resolution: + { + integrity: sha512-AZy/o6Uvjd46GWoisYLQBL37ET0T+XscI+E8nUuZGu/ihAhoZU3jxeE97WFPwBa5T3hrNd9L1XfLXTTDM2ZQWw== + } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1393,8 +2216,11 @@ packages: optional: true '@mdit/plugin-spoiler@0.14.0': - resolution: {integrity: sha512-c+tSj35JygQF9gNRAcH3gk68pBpjS7zGiooz/UK+tw8GSqez2c1Z+PWvu1u4xX0O/HO3y+amu7GqTjwkfgLC7g==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-c+tSj35JygQF9gNRAcH3gk68pBpjS7zGiooz/UK+tw8GSqez2c1Z+PWvu1u4xX0O/HO3y+amu7GqTjwkfgLC7g== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1402,8 +2228,11 @@ packages: optional: true '@mdit/plugin-stylize@0.14.0': - resolution: {integrity: sha512-oJFR+6JqWNnEjFsSO8FsMS92MNVzEaI+Re/3MX6OerJ7/ytdU8EkdgU8hcNcAGeT437SIKZsH9pRnxBupx0J7w==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-oJFR+6JqWNnEjFsSO8FsMS92MNVzEaI+Re/3MX6OerJ7/ytdU8EkdgU8hcNcAGeT437SIKZsH9pRnxBupx0J7w== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1411,8 +2240,11 @@ packages: optional: true '@mdit/plugin-sub@0.14.0': - resolution: {integrity: sha512-wva0pmw67t1aNZR7I+1GUyaWigJp+cGcPFvuwYviUuZh9npZXBIAdq1cCvycPaM29OT+LoBj5jdeydA81DKYSg==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-wva0pmw67t1aNZR7I+1GUyaWigJp+cGcPFvuwYviUuZh9npZXBIAdq1cCvycPaM29OT+LoBj5jdeydA81DKYSg== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1420,8 +2252,11 @@ packages: optional: true '@mdit/plugin-sup@0.14.0': - resolution: {integrity: sha512-QUwuCXQHS2atYv4SFfslVsuQLI1epDBie4Sw+E7XDwnbl9/o/vcJGkqdO4HKq8d73efowlWp2tGcpCxH+AeTWA==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-QUwuCXQHS2atYv4SFfslVsuQLI1epDBie4Sw+E7XDwnbl9/o/vcJGkqdO4HKq8d73efowlWp2tGcpCxH+AeTWA== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1429,7 +2264,10 @@ packages: optional: true '@mdit/plugin-tab@0.14.0': - resolution: {integrity: sha512-hY9sFejCGZPfHcEmk4WZ7EuTiw2EclD6zSO5FsuuuD8D5piQzI42UqlG5L+2TUOtC3gkeaNkKOSdLyahzMC6aw==} + resolution: + { + integrity: sha512-hY9sFejCGZPfHcEmk4WZ7EuTiw2EclD6zSO5FsuuuD8D5piQzI42UqlG5L+2TUOtC3gkeaNkKOSdLyahzMC6aw== + } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1437,8 +2275,11 @@ packages: optional: true '@mdit/plugin-tasklist@0.14.0': - resolution: {integrity: sha512-qKXt3td+A4pWbAYChPgOO+Ne9dOkb3SfrbyWE15M0AsIfVDizcq4wwVp8OQ25VAU5e4VisKiZ1XOE6xRoZHCbg==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-qKXt3td+A4pWbAYChPgOO+Ne9dOkb3SfrbyWE15M0AsIfVDizcq4wwVp8OQ25VAU5e4VisKiZ1XOE6xRoZHCbg== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1446,8 +2287,11 @@ packages: optional: true '@mdit/plugin-tex@0.14.0': - resolution: {integrity: sha512-fDigmoJLm3TzcS2nHrgc70EC680QTD9v+PlUDJX8gslE489+o0jRKB6/l6YuzBdpXNRcbrDNeo8q+oODlo/Y2w==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-fDigmoJLm3TzcS2nHrgc70EC680QTD9v+PlUDJX8gslE489+o0jRKB6/l6YuzBdpXNRcbrDNeo8q+oODlo/Y2w== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1455,8 +2299,11 @@ packages: optional: true '@mdit/plugin-uml@0.14.0': - resolution: {integrity: sha512-eIR4CiJfATlVAGuJh5Nd9L23zaN/ZDXc13SI/CaEPD+JG9/0h9XtyuceD5ncZDuEhmLnlxJua50Qek8NaObWDQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-eIR4CiJfATlVAGuJh5Nd9L23zaN/ZDXc13SI/CaEPD+JG9/0h9XtyuceD5ncZDuEhmLnlxJua50Qek8NaObWDQ== + } + engines: { node: '>= 18' } peerDependencies: markdown-it: ^14.1.0 peerDependenciesMeta: @@ -1464,25 +2311,43 @@ packages: optional: true '@microsoft/api-extractor-model@7.28.13': - resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} + resolution: + { + integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw== + } '@microsoft/api-extractor@7.43.0': - resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} + resolution: + { + integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w== + } hasBin: true '@microsoft/tsdoc-config@0.16.2': - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + resolution: + { + integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== + } '@microsoft/tsdoc@0.14.2': - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + resolution: + { + integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== + } '@nestjs/cli@9.5.0': - resolution: {integrity: sha512-Z7q+3vNsQSG2d2r2Hl/OOj5EpfjVx3OfnJ9+KuAsOdw1sKLm7+Zc6KbhMFTd/eIvfx82ww3Nk72xdmfPYCulWA==} - engines: {node: '>= 12.9.0'} + resolution: + { + integrity: sha512-Z7q+3vNsQSG2d2r2Hl/OOj5EpfjVx3OfnJ9+KuAsOdw1sKLm7+Zc6KbhMFTd/eIvfx82ww3Nk72xdmfPYCulWA== + } + engines: { node: '>= 12.9.0' } hasBin: true '@nestjs/common@11.1.12': - resolution: {integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==} + resolution: + { + integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw== + } peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -1495,7 +2360,10 @@ packages: optional: true '@nestjs/core@10.4.12': - resolution: {integrity: sha512-2qENES1U0VxCOfW45jVIV7g38edcvuU5aaLpZWOhb3nzehdvmeYoovUe8LTTHFrLWsDVY1v9FPfEhFQGsZfopQ==} + resolution: + { + integrity: sha512-2qENES1U0VxCOfW45jVIV7g38edcvuU5aaLpZWOhb3nzehdvmeYoovUe8LTTHFrLWsDVY1v9FPfEhFQGsZfopQ== + } peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/microservices': ^10.0.0 @@ -1512,18 +2380,27 @@ packages: optional: true '@nestjs/platform-express@11.1.19': - resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==} + resolution: + { + integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg== + } peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 '@nestjs/schematics@9.2.0': - resolution: {integrity: sha512-wHpNJDPzM6XtZUOB3gW0J6mkFCSJilzCM3XrHI1o0C8vZmFE1snbmkIXNyoi1eV0Nxh1BMymcgz5vIMJgQtTqw==} + resolution: + { + integrity: sha512-wHpNJDPzM6XtZUOB3gW0J6mkFCSJilzCM3XrHI1o0C8vZmFE1snbmkIXNyoi1eV0Nxh1BMymcgz5vIMJgQtTqw== + } peerDependencies: typescript: '>=4.3.5' '@nestjs/testing@9.4.3': - resolution: {integrity: sha512-LDT8Ai2eKnTzvnPaJwWOK03qTaFap5uHHsJCv6dL0uKWk6hyF9jms8DjyVaGsaujCaXDG8izl1mDEER0OmxaZA==} + resolution: + { + integrity: sha512-LDT8Ai2eKnTzvnPaJwWOK03qTaFap5uHHsJCv6dL0uKWk6hyF9jms8DjyVaGsaujCaXDG8izl1mDEER0OmxaZA== + } peerDependencies: '@nestjs/common': ^9.0.0 '@nestjs/core': ^9.0.0 @@ -1536,112 +2413,183 @@ packages: optional: true '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + resolution: + { + integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== + } '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + } + engines: { node: '>= 8' } '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + } + engines: { node: '>= 8' } '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + } + engines: { node: '>= 8' } '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} + resolution: + { + integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== + } + engines: { node: '>=12.4.0' } '@nuxtjs/opencollective@0.3.2': - resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} + resolution: + { + integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + } + engines: { node: '>=8.0.0', npm: '>=5.0.0' } hasBin: true '@parcel/watcher-android-arm64@2.4.1': - resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== + } + engines: { node: '>= 10.0.0' } cpu: [arm64] os: [android] '@parcel/watcher-darwin-arm64@2.4.1': - resolution: {integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== + } + engines: { node: '>= 10.0.0' } cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.4.1': - resolution: {integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== + } + engines: { node: '>= 10.0.0' } cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.4.1': - resolution: {integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== + } + engines: { node: '>= 10.0.0' } cpu: [x64] os: [freebsd] '@parcel/watcher-linux-arm-glibc@2.4.1': - resolution: {integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== + } + engines: { node: '>= 10.0.0' } cpu: [arm] os: [linux] '@parcel/watcher-linux-arm64-glibc@2.4.1': - resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== + } + engines: { node: '>= 10.0.0' } cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.4.1': - resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== + } + engines: { node: '>= 10.0.0' } cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.4.1': - resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== + } + engines: { node: '>= 10.0.0' } cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.4.1': - resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== + } + engines: { node: '>= 10.0.0' } cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.4.1': - resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== + } + engines: { node: '>= 10.0.0' } cpu: [arm64] os: [win32] '@parcel/watcher-win32-ia32@2.4.1': - resolution: {integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== + } + engines: { node: '>= 10.0.0' } cpu: [ia32] os: [win32] '@parcel/watcher-win32-x64@2.4.1': - resolution: {integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== + } + engines: { node: '>= 10.0.0' } cpu: [x64] os: [win32] '@parcel/watcher@2.4.1': - resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA== + } + engines: { node: '>= 10.0.0' } '@pkgr/core@0.1.1': - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + resolution: + { + integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + } + engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + + '@playwright/test@1.62.1': + resolution: + { + integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ== + } + engines: { node: '>=20' } + hasBin: true '@rollup/pluginutils@5.1.2': - resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw== + } + engines: { node: '>=14.0.0' } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: @@ -1649,218 +2597,350 @@ packages: optional: true '@rollup/rollup-android-arm-eabi@4.24.0': - resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} + resolution: + { + integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA== + } cpu: [arm] os: [android] '@rollup/rollup-android-arm-eabi@4.57.0': - resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==} + resolution: + { + integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA== + } cpu: [arm] os: [android] '@rollup/rollup-android-arm64@4.24.0': - resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} + resolution: + { + integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA== + } cpu: [arm64] os: [android] '@rollup/rollup-android-arm64@4.57.0': - resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==} + resolution: + { + integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg== + } cpu: [arm64] os: [android] '@rollup/rollup-darwin-arm64@4.24.0': - resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} + resolution: + { + integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== + } cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-arm64@4.57.0': - resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==} + resolution: + { + integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg== + } cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-x64@4.24.0': - resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} + resolution: + { + integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ== + } cpu: [x64] os: [darwin] '@rollup/rollup-darwin-x64@4.57.0': - resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==} + resolution: + { + integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA== + } cpu: [x64] os: [darwin] '@rollup/rollup-freebsd-arm64@4.57.0': - resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==} + resolution: + { + integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g== + } cpu: [arm64] os: [freebsd] '@rollup/rollup-freebsd-x64@4.57.0': - resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==} + resolution: + { + integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA== + } cpu: [x64] os: [freebsd] '@rollup/rollup-linux-arm-gnueabihf@4.24.0': - resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} + resolution: + { + integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA== + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm-gnueabihf@4.57.0': - resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} + resolution: + { + integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q== + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm-musleabihf@4.24.0': - resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} + resolution: + { + integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw== + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm-musleabihf@4.57.0': - resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} + resolution: + { + integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA== + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm64-gnu@4.24.0': - resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} + resolution: + { + integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA== + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-arm64-gnu@4.57.0': - resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} + resolution: + { + integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw== + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-arm64-musl@4.24.0': - resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} + resolution: + { + integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw== + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-arm64-musl@4.57.0': - resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} + resolution: + { + integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw== + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-loong64-gnu@4.57.0': - resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} + resolution: + { + integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q== + } cpu: [loong64] os: [linux] '@rollup/rollup-linux-loong64-musl@4.57.0': - resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} + resolution: + { + integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ== + } cpu: [loong64] os: [linux] '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': - resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} + resolution: + { + integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw== + } cpu: [ppc64] os: [linux] '@rollup/rollup-linux-ppc64-gnu@4.57.0': - resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} + resolution: + { + integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ== + } cpu: [ppc64] os: [linux] '@rollup/rollup-linux-ppc64-musl@4.57.0': - resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} + resolution: + { + integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA== + } cpu: [ppc64] os: [linux] '@rollup/rollup-linux-riscv64-gnu@4.24.0': - resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} + resolution: + { + integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg== + } cpu: [riscv64] os: [linux] '@rollup/rollup-linux-riscv64-gnu@4.57.0': - resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} + resolution: + { + integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ== + } cpu: [riscv64] os: [linux] '@rollup/rollup-linux-riscv64-musl@4.57.0': - resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} + resolution: + { + integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ== + } cpu: [riscv64] os: [linux] '@rollup/rollup-linux-s390x-gnu@4.24.0': - resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} + resolution: + { + integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g== + } cpu: [s390x] os: [linux] '@rollup/rollup-linux-s390x-gnu@4.57.0': - resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} + resolution: + { + integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg== + } cpu: [s390x] os: [linux] '@rollup/rollup-linux-x64-gnu@4.24.0': - resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} + resolution: + { + integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A== + } cpu: [x64] os: [linux] '@rollup/rollup-linux-x64-gnu@4.57.0': - resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} + resolution: + { + integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A== + } cpu: [x64] os: [linux] '@rollup/rollup-linux-x64-musl@4.24.0': - resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} + resolution: + { + integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ== + } cpu: [x64] os: [linux] '@rollup/rollup-linux-x64-musl@4.57.0': - resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} + resolution: + { + integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw== + } cpu: [x64] os: [linux] '@rollup/rollup-openbsd-x64@4.57.0': - resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} + resolution: + { + integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw== + } cpu: [x64] os: [openbsd] '@rollup/rollup-openharmony-arm64@4.57.0': - resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==} + resolution: + { + integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA== + } cpu: [arm64] os: [openharmony] '@rollup/rollup-win32-arm64-msvc@4.24.0': - resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} + resolution: + { + integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ== + } cpu: [arm64] os: [win32] '@rollup/rollup-win32-arm64-msvc@4.57.0': - resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==} + resolution: + { + integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ== + } cpu: [arm64] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.24.0': - resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} + resolution: + { + integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ== + } cpu: [ia32] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.57.0': - resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==} + resolution: + { + integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA== + } cpu: [ia32] os: [win32] '@rollup/rollup-win32-x64-gnu@4.57.0': - resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==} + resolution: + { + integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA== + } cpu: [x64] os: [win32] '@rollup/rollup-win32-x64-msvc@4.24.0': - resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} + resolution: + { + integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw== + } cpu: [x64] os: [win32] '@rollup/rollup-win32-x64-msvc@4.57.0': - resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==} + resolution: + { + integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ== + } cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + resolution: + { + integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + } '@rushstack/eslint-patch@1.10.4': - resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + resolution: + { + integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== + } '@rushstack/node-core-library@4.0.2': - resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} + resolution: + { + integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg== + } peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -1868,10 +2948,16 @@ packages: optional: true '@rushstack/rig-package@0.5.2': - resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + resolution: + { + integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA== + } '@rushstack/terminal@0.10.0': - resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} + resolution: + { + integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw== + } peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -1879,295 +2965,580 @@ packages: optional: true '@rushstack/ts-command-line@4.19.1': - resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} + resolution: + { + integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg== + } '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + resolution: + { + integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== + } '@shikijs/core@1.24.1': - resolution: {integrity: sha512-3q/9oarMVcLqJ+NQOdKL40dJVq/UKCsiWXz3QRQPBglHqa8dDJ0p6TuMuk2gHphy5FZcvFtg4UHBgpW0JtZ8+A==} + resolution: + { + integrity: sha512-3q/9oarMVcLqJ+NQOdKL40dJVq/UKCsiWXz3QRQPBglHqa8dDJ0p6TuMuk2gHphy5FZcvFtg4UHBgpW0JtZ8+A== + } '@shikijs/engine-javascript@1.24.1': - resolution: {integrity: sha512-lNgUSHYDYaQ6daj4lJJqcY2Ru9LgHwpFoposJkRVRPh21Yg4kaPFRhzaWoSg3PliwcDOpDuMy3xsmQaJp201Fg==} + resolution: + { + integrity: sha512-lNgUSHYDYaQ6daj4lJJqcY2Ru9LgHwpFoposJkRVRPh21Yg4kaPFRhzaWoSg3PliwcDOpDuMy3xsmQaJp201Fg== + } '@shikijs/engine-oniguruma@1.24.1': - resolution: {integrity: sha512-KdrTIBIONWd+Xs61eh8HdIpfigtrseat9dpARvaOe2x0g/FNTbwbkGr3y92VSOVD1XotzEskh3v/nCzyWjkf7g==} + resolution: + { + integrity: sha512-KdrTIBIONWd+Xs61eh8HdIpfigtrseat9dpARvaOe2x0g/FNTbwbkGr3y92VSOVD1XotzEskh3v/nCzyWjkf7g== + } '@shikijs/transformers@1.24.1': - resolution: {integrity: sha512-IQmk8o0RNlaQ3Ig0Urx4bfen/zvS5St6as7o0q+bTDfbuFjqUMNjoMMKMN1Lu66mAh83seURwPLi2EjjLTD6+w==} + resolution: + { + integrity: sha512-IQmk8o0RNlaQ3Ig0Urx4bfen/zvS5St6as7o0q+bTDfbuFjqUMNjoMMKMN1Lu66mAh83seURwPLi2EjjLTD6+w== + } '@shikijs/types@1.24.1': - resolution: {integrity: sha512-ZwZFbShFY/APfKNt3s9Gv8rhTm29GodSKsOW66X6N+HGsZuaHalE1VUEX4fv93UXHTZTLjb3uxn63F96RhGfXw==} + resolution: + { + integrity: sha512-ZwZFbShFY/APfKNt3s9Gv8rhTm29GodSKsOW66X6N+HGsZuaHalE1VUEX4fv93UXHTZTLjb3uxn63F96RhGfXw== + } '@shikijs/vscode-textmate@9.3.0': - resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + resolution: + { + integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== + } '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + resolution: + { + integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + } '@sindresorhus/is@7.0.1': - resolution: {integrity: sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ== + } + engines: { node: '>=18' } '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== + } + engines: { node: '>=18' } '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ== + } + engines: { node: '>=18' } '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + resolution: + { + integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + } '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + resolution: + { + integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + } '@stackblitz/sdk@1.11.0': - resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + resolution: + { + integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ== + } '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + resolution: + { + integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + } '@szmarczak/http-timer@5.0.1': - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + } + engines: { node: '>=14.16' } '@tokenizer/inflate@0.4.1': - resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA== + } + engines: { node: '>=18' } '@tokenizer/token@0.3.0': - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + resolution: + { + integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + } '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + resolution: + { + integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + } '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + resolution: + { + integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + } '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + resolution: + { + integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + } '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + resolution: + { + integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + } '@types/accepts@1.3.7': - resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + resolution: + { + integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ== + } '@types/argparse@1.0.38': - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + resolution: + { + integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== + } '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + } '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + resolution: + { + integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + } '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + } '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + resolution: + { + integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + } '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + resolution: + { + integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + } '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + resolution: + { + integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + } '@types/co-body@6.1.3': - resolution: {integrity: sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==} + resolution: + { + integrity: sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA== + } '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + resolution: + { + integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + } '@types/content-disposition@0.5.8': - resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==} + resolution: + { + integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg== + } '@types/conventional-commits-parser@5.0.0': - resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + resolution: + { + integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ== + } '@types/cookiejar@2.1.5': - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + resolution: + { + integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + } '@types/cookies@0.9.0': - resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} + resolution: + { + integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q== + } '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + resolution: + { + integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + } '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + resolution: + { + integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + } '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + resolution: + { + integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + } '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + resolution: + { + integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + } '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + resolution: + { + integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + } '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + } '@types/express-serve-static-core@4.19.6': - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + resolution: + { + integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + } '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + resolution: + { + integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + } '@types/formidable@2.0.6': - resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==} + resolution: + { + integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w== + } '@types/fs-extra@11.0.4': - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + resolution: + { + integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + } '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + resolution: + { + integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + } '@types/hash-sum@1.0.2': - resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} + resolution: + { + integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw== + } '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + resolution: + { + integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + } '@types/http-assert@1.5.5': - resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} + resolution: + { + integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g== + } '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + resolution: + { + integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + } '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + resolution: + { + integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + } '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + resolution: + { + integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + } '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + resolution: + { + integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + } '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + resolution: + { + integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + } '@types/jest@29.2.4': - resolution: {integrity: sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==} + resolution: + { + integrity: sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A== + } '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + } '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + resolution: + { + integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + } '@types/jsonfile@6.1.4': - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + resolution: + { + integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + } '@types/katex@0.16.7': - resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + resolution: + { + integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== + } '@types/keygrip@1.0.6': - resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + resolution: + { + integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ== + } '@types/koa-compose@3.2.8': - resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} + resolution: + { + integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA== + } '@types/koa-router@7.4.8': - resolution: {integrity: sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A==} + resolution: + { + integrity: sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A== + } '@types/koa@2.15.0': - resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} + resolution: + { + integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g== + } '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + resolution: + { + integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + } '@types/markdown-it-emoji@3.0.1': - resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} + resolution: + { + integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA== + } '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + resolution: + { + integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + } '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + resolution: + { + integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + } '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + resolution: + { + integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + } '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + resolution: + { + integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + } '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + resolution: + { + integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + } '@types/ms@0.7.34': - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + resolution: + { + integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + } '@types/node@17.0.45': - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + resolution: + { + integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + } '@types/node@18.11.18': - resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + resolution: + { + integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + } '@types/node@20.16.11': - resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} + resolution: + { + integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw== + } '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + resolution: + { + integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + } '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + resolution: + { + integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== + } '@types/parse-json@4.0.2': - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + resolution: + { + integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + } '@types/qs@6.9.16': - resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + resolution: + { + integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== + } '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + resolution: + { + integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + } '@types/sax@1.2.7': - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + resolution: + { + integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== + } '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + resolution: + { + integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + } '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + resolution: + { + integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + } '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + resolution: + { + integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + } '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + resolution: + { + integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + } '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + resolution: + { + integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + } '@types/supertest@2.0.16': - resolution: {integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==} + resolution: + { + integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg== + } '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + resolution: + { + integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + } '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + resolution: + { + integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + } '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + resolution: + { + integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + } '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + resolution: + { + integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + } '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + resolution: + { + integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + } '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + resolution: + { + integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + } '@typescript-eslint/eslint-plugin@5.62.0': - resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: '@typescript-eslint/parser': ^5.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2177,8 +3548,11 @@ packages: optional: true '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== + } + engines: { node: ^16.0.0 || >=18.0.0 } peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha eslint: ^7.0.0 || ^8.0.0 @@ -2188,8 +3562,11 @@ packages: optional: true '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== + } + engines: { node: ^18.18.0 || >=20.0.0 } peerDependencies: '@typescript-eslint/parser': ^7.0.0 eslint: ^8.56.0 @@ -2199,8 +3576,11 @@ packages: optional: true '@typescript-eslint/parser@5.62.0': - resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' @@ -2209,8 +3589,11 @@ packages: optional: true '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + } + engines: { node: ^16.0.0 || >=18.0.0 } peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: '*' @@ -2219,8 +3602,11 @@ packages: optional: true '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== + } + engines: { node: ^18.18.0 || >=20.0.0 } peerDependencies: eslint: ^8.56.0 typescript: '*' @@ -2229,20 +3615,32 @@ packages: optional: true '@typescript-eslint/scope-manager@5.62.0': - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + } + engines: { node: ^16.0.0 || >=18.0.0 } '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA== + } + engines: { node: ^18.18.0 || >=20.0.0 } '@typescript-eslint/type-utils@5.62.0': - resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: '*' typescript: '*' @@ -2251,8 +3649,11 @@ packages: optional: true '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== + } + engines: { node: ^16.0.0 || >=18.0.0 } peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: '*' @@ -2261,8 +3662,11 @@ packages: optional: true '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== + } + engines: { node: ^18.18.0 || >=20.0.0 } peerDependencies: eslint: ^8.56.0 typescript: '*' @@ -2271,20 +3675,32 @@ packages: optional: true '@typescript-eslint/types@5.62.0': - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + } + engines: { node: ^16.0.0 || >=18.0.0 } '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== + } + engines: { node: ^18.18.0 || >=20.0.0 } '@typescript-eslint/typescript-estree@5.62.0': - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2292,8 +3708,11 @@ packages: optional: true '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + } + engines: { node: ^16.0.0 || >=18.0.0 } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2301,8 +3720,11 @@ packages: optional: true '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA== + } + engines: { node: ^18.18.0 || >=20.0.0 } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2310,41 +3732,66 @@ packages: optional: true '@typescript-eslint/utils@5.62.0': - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + } + engines: { node: ^16.0.0 || >=18.0.0 } peerDependencies: eslint: ^7.0.0 || ^8.0.0 '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== + } + engines: { node: ^18.18.0 || >=20.0.0 } peerDependencies: eslint: ^8.56.0 '@typescript-eslint/visitor-keys@5.62.0': - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + resolution: + { + integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + } + engines: { node: ^16.0.0 || >=18.0.0 } '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} + resolution: + { + integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg== + } + engines: { node: ^18.18.0 || >=20.0.0 } '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + resolution: + { + integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + } + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vercel/style-guide@5.2.0': - resolution: {integrity: sha512-fNSKEaZvSkiBoF6XEefs8CcgAV9K9e+MbcsDZjUsktHycKdA0jvjAzQi1W/FzLS+Nr5zZ6oejCwq/97dHUKe0g==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-fNSKEaZvSkiBoF6XEefs8CcgAV9K9e+MbcsDZjUsktHycKdA0jvjAzQi1W/FzLS+Nr5zZ6oejCwq/97dHUKe0g== + } + engines: { node: '>=16' } peerDependencies: '@next/eslint-plugin-next': '>=12.3.0 <15' eslint: '>=8.48.0 <9' @@ -2361,14 +3808,20 @@ packages: optional: true '@vitejs/plugin-vue@5.1.4': - resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A== + } + engines: { node: ^18.0.0 || >=20.0.0 } peerDependencies: vite: ^5.0.0 vue: ^3.2.25 '@vitest/coverage-v8@4.0.18': - resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + resolution: + { + integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg== + } peerDependencies: '@vitest/browser': 4.0.18 vitest: 4.0.18 @@ -2377,16 +3830,28 @@ packages: optional: true '@vitest/expect@1.6.0': - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + resolution: + { + integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== + } '@vitest/expect@2.1.4': - resolution: {integrity: sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==} + resolution: + { + integrity: sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA== + } '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + resolution: + { + integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== + } '@vitest/mocker@2.1.4': - resolution: {integrity: sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==} + resolution: + { + integrity: sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ== + } peerDependencies: msw: ^2.4.9 vite: ^5.0.0 @@ -2397,7 +3862,10 @@ packages: optional: true '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + resolution: + { + integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ== + } peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -2408,103 +3876,202 @@ packages: optional: true '@vitest/pretty-format@2.1.4': - resolution: {integrity: sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==} + resolution: + { + integrity: sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww== + } '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + resolution: + { + integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== + } '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + resolution: + { + integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg== + } '@vitest/runner@2.1.4': - resolution: {integrity: sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==} + resolution: + { + integrity: sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA== + } '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + resolution: + { + integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw== + } '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + resolution: + { + integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ== + } '@vitest/snapshot@2.1.4': - resolution: {integrity: sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==} + resolution: + { + integrity: sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q== + } '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + resolution: + { + integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA== + } '@vitest/spy@1.6.0': - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + resolution: + { + integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== + } '@vitest/spy@2.1.4': - resolution: {integrity: sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==} + resolution: + { + integrity: sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg== + } '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + resolution: + { + integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== + } '@vitest/utils@1.6.0': - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + resolution: + { + integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== + } '@vitest/utils@2.1.4': - resolution: {integrity: sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==} + resolution: + { + integrity: sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg== + } '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + resolution: + { + integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== + } '@volar/language-core@1.11.1': - resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + resolution: + { + integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw== + } '@volar/source-map@1.11.1': - resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + resolution: + { + integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg== + } '@volar/typescript@1.11.1': - resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + resolution: + { + integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ== + } '@vue/compiler-core@3.5.12': - resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==} + resolution: + { + integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw== + } '@vue/compiler-core@3.5.13': - resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + resolution: + { + integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== + } '@vue/compiler-dom@3.5.12': - resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==} + resolution: + { + integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg== + } '@vue/compiler-dom@3.5.13': - resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + resolution: + { + integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + } '@vue/compiler-sfc@3.5.12': - resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==} + resolution: + { + integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw== + } '@vue/compiler-sfc@3.5.13': - resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + resolution: + { + integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + } '@vue/compiler-ssr@3.5.12': - resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==} + resolution: + { + integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA== + } '@vue/compiler-ssr@3.5.13': - resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + resolution: + { + integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + } '@vue/devtools-api@6.6.4': - resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + resolution: + { + integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== + } '@vue/devtools-api@7.4.6': - resolution: {integrity: sha512-XipBV5k0/IfTr0sNBDTg7OBUCp51cYMMXyPxLXJZ4K/wmUeMqt8cVdr2ZZGOFq+si/jTyCYnNxeKoyev5DOUUA==} + resolution: + { + integrity: sha512-XipBV5k0/IfTr0sNBDTg7OBUCp51cYMMXyPxLXJZ4K/wmUeMqt8cVdr2ZZGOFq+si/jTyCYnNxeKoyev5DOUUA== + } '@vue/devtools-api@7.6.7': - resolution: {integrity: sha512-PV4I31WaV2rfA8RGauM+69uFEzWkqtP561RiLU2wK+Ce85u3zyKW3aoESlLCNzkc4y0JaJyskH6zAE3xWOP8+Q==} + resolution: + { + integrity: sha512-PV4I31WaV2rfA8RGauM+69uFEzWkqtP561RiLU2wK+Ce85u3zyKW3aoESlLCNzkc4y0JaJyskH6zAE3xWOP8+Q== + } '@vue/devtools-kit@7.4.6': - resolution: {integrity: sha512-NbYBwPWgEic1AOd9bWExz9weBzFdjiIfov0yRn4DrRfR+EQJCI9dn4I0XS7IxYGdkmUJi8mFW42LLk18WsGqew==} + resolution: + { + integrity: sha512-NbYBwPWgEic1AOd9bWExz9weBzFdjiIfov0yRn4DrRfR+EQJCI9dn4I0XS7IxYGdkmUJi8mFW42LLk18WsGqew== + } '@vue/devtools-kit@7.6.7': - resolution: {integrity: sha512-V8/jrXY/swHgnblABG9U4QCbE60c6RuPasmv2d9FvVqc5d94t1vDiESuvRmdNJBdWz4/D3q6ffgyAfRVjwHYEw==} + resolution: + { + integrity: sha512-V8/jrXY/swHgnblABG9U4QCbE60c6RuPasmv2d9FvVqc5d94t1vDiESuvRmdNJBdWz4/D3q6ffgyAfRVjwHYEw== + } '@vue/devtools-shared@7.4.6': - resolution: {integrity: sha512-rPeSBzElnHYMB05Cc056BQiJpgocQjY8XVulgni+O9a9Gr9tNXgPteSzFFD+fT/iWMxNuUgGKs9CuW5DZewfIg==} + resolution: + { + integrity: sha512-rPeSBzElnHYMB05Cc056BQiJpgocQjY8XVulgni+O9a9Gr9tNXgPteSzFFD+fT/iWMxNuUgGKs9CuW5DZewfIg== + } '@vue/devtools-shared@7.6.7': - resolution: {integrity: sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw==} + resolution: + { + integrity: sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw== + } '@vue/language-core@1.8.27': - resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + resolution: + { + integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA== + } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2512,67 +4079,121 @@ packages: optional: true '@vue/reactivity@3.5.12': - resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==} + resolution: + { + integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg== + } '@vue/reactivity@3.5.13': - resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + resolution: + { + integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg== + } '@vue/runtime-core@3.5.12': - resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==} + resolution: + { + integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw== + } '@vue/runtime-core@3.5.13': - resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + resolution: + { + integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw== + } '@vue/runtime-dom@3.5.12': - resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==} + resolution: + { + integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA== + } '@vue/runtime-dom@3.5.13': - resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + resolution: + { + integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog== + } '@vue/server-renderer@3.5.12': - resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==} + resolution: + { + integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg== + } peerDependencies: vue: 3.5.12 '@vue/server-renderer@3.5.13': - resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + resolution: + { + integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA== + } peerDependencies: vue: 3.5.13 '@vue/shared@3.5.12': - resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==} + resolution: + { + integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg== + } '@vue/shared@3.5.13': - resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + resolution: + { + integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== + } '@vuepress/bundler-vite@2.0.0-rc.18': - resolution: {integrity: sha512-Q+OUul4OLIS4OLuKqIlmJKHhW5Edt5i6fVY6infgGhb4tUQt3z37DjCUtvbMikb05Va9YqtTAGt2eCWOk7eGPw==} + resolution: + { + integrity: sha512-Q+OUul4OLIS4OLuKqIlmJKHhW5Edt5i6fVY6infgGhb4tUQt3z37DjCUtvbMikb05Va9YqtTAGt2eCWOk7eGPw== + } '@vuepress/bundlerutils@2.0.0-rc.18': - resolution: {integrity: sha512-GTcdc78cfcKqn/D3xPrxGFeR+WPV2zJE82jpKAnIa4I30aScq/95pYF1FofP0Gdc+0/XQCxFQ8xiT8iYcoQPSw==} + resolution: + { + integrity: sha512-GTcdc78cfcKqn/D3xPrxGFeR+WPV2zJE82jpKAnIa4I30aScq/95pYF1FofP0Gdc+0/XQCxFQ8xiT8iYcoQPSw== + } '@vuepress/cli@2.0.0-rc.18': - resolution: {integrity: sha512-9Oxyb22klpBNzia+2D4NRGv+Jk0+TwHX8Pn25cy9TlyxMeh9+jFioXT0Jc3c9/vOesBaCI6JxSxwPqtgRFr1rQ==} + resolution: + { + integrity: sha512-9Oxyb22klpBNzia+2D4NRGv+Jk0+TwHX8Pn25cy9TlyxMeh9+jFioXT0Jc3c9/vOesBaCI6JxSxwPqtgRFr1rQ== + } hasBin: true '@vuepress/client@2.0.0-rc.18': - resolution: {integrity: sha512-ImeF10uwZvQt3KyYN+fdyPRCZmzEJ2r4sTxC/39ieVA4BzPpTzrJwBNT3KONYkckaoXnBXIoI8d+DWFfq9B9NQ==} + resolution: + { + integrity: sha512-ImeF10uwZvQt3KyYN+fdyPRCZmzEJ2r4sTxC/39ieVA4BzPpTzrJwBNT3KONYkckaoXnBXIoI8d+DWFfq9B9NQ== + } '@vuepress/core@2.0.0-rc.18': - resolution: {integrity: sha512-ikQ5EyA1jZynk1amsihG0cX6kWTgCIsbGCBgPWDVfkSPCrYCxxaIfzvKgyGBiNBFZ7kqoxuMnEn8EaY/fhSL0A==} + resolution: + { + integrity: sha512-ikQ5EyA1jZynk1amsihG0cX6kWTgCIsbGCBgPWDVfkSPCrYCxxaIfzvKgyGBiNBFZ7kqoxuMnEn8EaY/fhSL0A== + } '@vuepress/helper@2.0.0-rc.63': - resolution: {integrity: sha512-42GmRhRttE2sgM4qUUF4PCXMYIxhgXDWrwa+gNbQmvrcs16saeKEQ4iL8lI6JITcy9S7+M6NtndbFkFwRlAscg==} + resolution: + { + integrity: sha512-42GmRhRttE2sgM4qUUF4PCXMYIxhgXDWrwa+gNbQmvrcs16saeKEQ4iL8lI6JITcy9S7+M6NtndbFkFwRlAscg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/helper@2.0.0-rc.65': - resolution: {integrity: sha512-AeUABMaFkkSSVNN+kwaozSYtSpohXc9GLg4792P/C6X+sOow/KauOMP/A4LryuoVFjtEB9rjeQgujnb9Fn2bmQ==} + resolution: + { + integrity: sha512-AeUABMaFkkSSVNN+kwaozSYtSpohXc9GLg4792P/C6X+sOow/KauOMP/A4LryuoVFjtEB9rjeQgujnb9Fn2bmQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/highlighter-helper@2.0.0-rc.62': - resolution: {integrity: sha512-tWLz314Dzr8dUDDBDmsjnsWVi6AJwXyWFMo2zsYnZGk5v2ZovQvigrtqjxB7OIw2LlizAWGwx4gv8jt7fsxCdQ==} + resolution: + { + integrity: sha512-tWLz314Dzr8dUDDBDmsjnsWVi6AJwXyWFMo2zsYnZGk5v2ZovQvigrtqjxB7OIw2LlizAWGwx4gv8jt7fsxCdQ== + } peerDependencies: '@vueuse/core': ^12.0.0 vuepress: 2.0.0-rc.18 @@ -2581,30 +4202,48 @@ packages: optional: true '@vuepress/markdown@2.0.0-rc.18': - resolution: {integrity: sha512-RUX7rgjONBwOepWXqB4SzI2Tbm6zEYK2YTHwjexzAIBr+nxgB+B8nizdr+VvuVk7Ehn/CtcyXhBdf1NZh9UgUQ==} + resolution: + { + integrity: sha512-RUX7rgjONBwOepWXqB4SzI2Tbm6zEYK2YTHwjexzAIBr+nxgB+B8nizdr+VvuVk7Ehn/CtcyXhBdf1NZh9UgUQ== + } '@vuepress/plugin-active-header-links@2.0.0-rc.63': - resolution: {integrity: sha512-D2VtuOYmnmWRgaUeFjt09HAEaPWFT8l9tjpF6gX29eJPer0QsPjM5Wau+cTvl3cmVh+dnDUhiYOIc7MwYEU2bQ==} + resolution: + { + integrity: sha512-D2VtuOYmnmWRgaUeFjt09HAEaPWFT8l9tjpF6gX29eJPer0QsPjM5Wau+cTvl3cmVh+dnDUhiYOIc7MwYEU2bQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-back-to-top@2.0.0-rc.65': - resolution: {integrity: sha512-+PcmO8JQO8cswCGXOyYQI4MnCT4HwMIAzuVI8nR46Q4Ime+Ye5VsC12aXi479A9npoXhtRwMDkmCFJfCy7I3+Q==} + resolution: + { + integrity: sha512-+PcmO8JQO8cswCGXOyYQI4MnCT4HwMIAzuVI8nR46Q4Ime+Ye5VsC12aXi479A9npoXhtRwMDkmCFJfCy7I3+Q== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-blog@2.0.0-rc.65': - resolution: {integrity: sha512-5ZQZ1bT1zeF9JeqRoIbJQLiIdHI7SMg652Ml880U0u4K3pQsE2MERCKHtDpAh98afG0XvDTXxyS0i53fCpPt6g==} + resolution: + { + integrity: sha512-5ZQZ1bT1zeF9JeqRoIbJQLiIdHI7SMg652Ml880U0u4K3pQsE2MERCKHtDpAh98afG0XvDTXxyS0i53fCpPt6g== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-catalog@2.0.0-rc.65': - resolution: {integrity: sha512-33mTadR0pKi82/mQReYcjxV6uNcC9Fe1kLHBek0VGQEMqRk2rTegc7jjBlEDOQmTAbrraqqiZpnPZ+xVWETxUQ==} + resolution: + { + integrity: sha512-33mTadR0pKi82/mQReYcjxV6uNcC9Fe1kLHBek0VGQEMqRk2rTegc7jjBlEDOQmTAbrraqqiZpnPZ+xVWETxUQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-comment@2.0.0-rc.65': - resolution: {integrity: sha512-flsSpYVUNtTvT19lEADfD7102qL8mZMabpLmx6TnYRfFTdRczoirfsLK+DF/7z/ELNQqbyQ8BTwnq3clNOGwSg==} + resolution: + { + integrity: sha512-flsSpYVUNtTvT19lEADfD7102qL8mZMabpLmx6TnYRfFTdRczoirfsLK+DF/7z/ELNQqbyQ8BTwnq3clNOGwSg== + } peerDependencies: '@waline/client': ^3.4.1 artalk: ^2.9.0 @@ -2619,47 +4258,74 @@ packages: optional: true '@vuepress/plugin-copy-code@2.0.0-rc.65': - resolution: {integrity: sha512-3lKa7OIwBC5OcWrR5+cFUmUZz8ESQI+tcfDCt5B3foKOFkAlMcZVLBb4ZJyorQG6UYn9soNuhbzMWtfWDqNIoA==} + resolution: + { + integrity: sha512-3lKa7OIwBC5OcWrR5+cFUmUZz8ESQI+tcfDCt5B3foKOFkAlMcZVLBb4ZJyorQG6UYn9soNuhbzMWtfWDqNIoA== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-copyright@2.0.0-rc.65': - resolution: {integrity: sha512-YQaEL6j5ri+lC48kvKfmLn+KW8PD9cOfGdwlNZijqrBTCt11icb29rv4iNv6yGMn1vdFkPzxOj54/jJzOw9gsg==} + resolution: + { + integrity: sha512-YQaEL6j5ri+lC48kvKfmLn+KW8PD9cOfGdwlNZijqrBTCt11icb29rv4iNv6yGMn1vdFkPzxOj54/jJzOw9gsg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-git@2.0.0-rc.62': - resolution: {integrity: sha512-V1sG1NlrWjDlOJtUoCx2BjVdgokog3A7buJztyOtbNyRLWYY5EALUOgWrF4Ccf3TFVdTxDgV/vm5U23ycsO83w==} + resolution: + { + integrity: sha512-V1sG1NlrWjDlOJtUoCx2BjVdgokog3A7buJztyOtbNyRLWYY5EALUOgWrF4Ccf3TFVdTxDgV/vm5U23ycsO83w== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-links-check@2.0.0-rc.65': - resolution: {integrity: sha512-Z30L24y6Q/EE6VLZo2aiTniAgAtkXXwWcM/Mi1CMCAP0cItCtT4ZF7DWdBi35QhFw4HtdAuq0DSkSK0y22auGg==} + resolution: + { + integrity: sha512-Z30L24y6Q/EE6VLZo2aiTniAgAtkXXwWcM/Mi1CMCAP0cItCtT4ZF7DWdBi35QhFw4HtdAuq0DSkSK0y22auGg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-ext@2.0.0-rc.65': - resolution: {integrity: sha512-rEb3xD1JTdH34PAPjdtQDIYiaDXNiCAumYUgOIYmDILUYbHE/BmFNejvE8AbPfgtG7UimYOfoa6JGuGbdnVbcw==} + resolution: + { + integrity: sha512-rEb3xD1JTdH34PAPjdtQDIYiaDXNiCAumYUgOIYmDILUYbHE/BmFNejvE8AbPfgtG7UimYOfoa6JGuGbdnVbcw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-hint@2.0.0-rc.65': - resolution: {integrity: sha512-OqrcSDJVl5qTsSI450/KlBXqjqxJ2+s8YynjnRwteWj3jtlsOpi66voB49A87gi+vgyjw0B88BBsZrF9mmkQTA==} + resolution: + { + integrity: sha512-OqrcSDJVl5qTsSI450/KlBXqjqxJ2+s8YynjnRwteWj3jtlsOpi66voB49A87gi+vgyjw0B88BBsZrF9mmkQTA== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-image@2.0.0-rc.65': - resolution: {integrity: sha512-DsY4izZ4dw+BthVHiQr7CknPazUuCJT3Z1+iEF7NtTPI0kfRROABgwcMUybZ8ZO6qyKuDtgxuv2xck8yI/7HXg==} + resolution: + { + integrity: sha512-DsY4izZ4dw+BthVHiQr7CknPazUuCJT3Z1+iEF7NtTPI0kfRROABgwcMUybZ8ZO6qyKuDtgxuv2xck8yI/7HXg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-include@2.0.0-rc.65': - resolution: {integrity: sha512-UxjMnjSjLcEsB0PKgch7iXkGt0WEltZWFSSeWeSGtlTVwZdMhVYMzxw0R/WOaVs5G+fLTxWPtYaE1v33zgAt/w==} + resolution: + { + integrity: sha512-UxjMnjSjLcEsB0PKgch7iXkGt0WEltZWFSSeWeSGtlTVwZdMhVYMzxw0R/WOaVs5G+fLTxWPtYaE1v33zgAt/w== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-math@2.0.0-rc.65': - resolution: {integrity: sha512-GL8yhVE3hZQyCOTKxJAvEDzkqrXGrzMJYK3TT6nCOZR1g8JUL/d+J+E5TUPTYQiSxQMH33jNNyoXmTgCPZloqw==} + resolution: + { + integrity: sha512-GL8yhVE3hZQyCOTKxJAvEDzkqrXGrzMJYK3TT6nCOZR1g8JUL/d+J+E5TUPTYQiSxQMH33jNNyoXmTgCPZloqw== + } peerDependencies: katex: ^0.16.10 mathjax-full: ^3.2.2 @@ -2671,63 +4337,99 @@ packages: optional: true '@vuepress/plugin-markdown-stylize@2.0.0-rc.65': - resolution: {integrity: sha512-liFMN6WOjedUaHBXuhS0O78Zj+XJ1bFJAhbtkvFBPCMYIWt3CEKYsOIiCwuAYdQAm06od+mHQuuXMyFEJcnUUQ==} + resolution: + { + integrity: sha512-liFMN6WOjedUaHBXuhS0O78Zj+XJ1bFJAhbtkvFBPCMYIWt3CEKYsOIiCwuAYdQAm06od+mHQuuXMyFEJcnUUQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-markdown-tab@2.0.0-rc.65': - resolution: {integrity: sha512-yFkdX3Ifyzo41lhvIuOT5NRTL/NcPYPltJPRsMqMywHHZ7UdM+Ku8iCt+USwWut/0eaoY5bNqu5CugUZphkEHA==} + resolution: + { + integrity: sha512-yFkdX3Ifyzo41lhvIuOT5NRTL/NcPYPltJPRsMqMywHHZ7UdM+Ku8iCt+USwWut/0eaoY5bNqu5CugUZphkEHA== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-medium-zoom@2.0.0-rc.65': - resolution: {integrity: sha512-r8I4Rqtx4ttCXvuPKSrrWxxb5J7PjnCfy6LCErjRGA9o7y3AfTmbyBhvDQTPQLi8Yq1Y96j7XO1EoEtwCAsPfw==} + resolution: + { + integrity: sha512-r8I4Rqtx4ttCXvuPKSrrWxxb5J7PjnCfy6LCErjRGA9o7y3AfTmbyBhvDQTPQLi8Yq1Y96j7XO1EoEtwCAsPfw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-notice@2.0.0-rc.65': - resolution: {integrity: sha512-BA4bOnR6ynqecayQWU1rH3v0ErZVEiXYU7TugH/OWfrprZV0QTKQmxD9P0qeSft6gfSVe161iWQF+Zxp1GoM8w==} + resolution: + { + integrity: sha512-BA4bOnR6ynqecayQWU1rH3v0ErZVEiXYU7TugH/OWfrprZV0QTKQmxD9P0qeSft6gfSVe161iWQF+Zxp1GoM8w== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-nprogress@2.0.0-rc.65': - resolution: {integrity: sha512-B5mf7Zn7muu3yP9HP6Z/3GuqHPDcfEkHPrhiqPq/5crhot1enON5LkoGmfQ3lAXZQSzvrWL+2Os85adgtOV7mA==} + resolution: + { + integrity: sha512-B5mf7Zn7muu3yP9HP6Z/3GuqHPDcfEkHPrhiqPq/5crhot1enON5LkoGmfQ3lAXZQSzvrWL+2Os85adgtOV7mA== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-palette@2.0.0-rc.65': - resolution: {integrity: sha512-H85TsjdnfxS1dr92ggYKkJFw4S057AKvjQbbXG3BsuAcvtDLAWYhxhs1T+o50bPGnuHZr1W/rSKxYWCYFhJlbQ==} + resolution: + { + integrity: sha512-H85TsjdnfxS1dr92ggYKkJFw4S057AKvjQbbXG3BsuAcvtDLAWYhxhs1T+o50bPGnuHZr1W/rSKxYWCYFhJlbQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-photo-swipe@2.0.0-rc.65': - resolution: {integrity: sha512-BnYl+BJ5FZcMFbQTDfnpFf9sNxV0doFgPmOkWuijZ/372dQJoFJmGVWacTiKRkDwJLfZjHXpxgb+4GQqcYsYkg==} + resolution: + { + integrity: sha512-BnYl+BJ5FZcMFbQTDfnpFf9sNxV0doFgPmOkWuijZ/372dQJoFJmGVWacTiKRkDwJLfZjHXpxgb+4GQqcYsYkg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-prismjs@2.0.0-rc.65': - resolution: {integrity: sha512-yJf6NCc2VRJ1rEyTN4b+WBQIRd/eizNusrUHCRECWaeyP4Y2vELBJ8CFXl5pxfyKVzuJYgKOAtpxabzDC6pOHA==} + resolution: + { + integrity: sha512-yJf6NCc2VRJ1rEyTN4b+WBQIRd/eizNusrUHCRECWaeyP4Y2vELBJ8CFXl5pxfyKVzuJYgKOAtpxabzDC6pOHA== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-reading-time@2.0.0-rc.65': - resolution: {integrity: sha512-gEoqdfXQonsx3WHoOITNmCNQiJ8frMbwiMJ+1zMO0UC3touIv8S97wc0lxormI1T03BVFMVwOPMwCjr1NLkqgw==} + resolution: + { + integrity: sha512-gEoqdfXQonsx3WHoOITNmCNQiJ8frMbwiMJ+1zMO0UC3touIv8S97wc0lxormI1T03BVFMVwOPMwCjr1NLkqgw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-redirect@2.0.0-rc.65': - resolution: {integrity: sha512-m5P02DM5AikRN8kSw1mFt61AG0Uv32z3Imn3dB0EgYi/E7sobegjZ7OnxdqN/gRfjI5QUHd4MBJLAqfPffOdqw==} + resolution: + { + integrity: sha512-m5P02DM5AikRN8kSw1mFt61AG0Uv32z3Imn3dB0EgYi/E7sobegjZ7OnxdqN/gRfjI5QUHd4MBJLAqfPffOdqw== + } hasBin: true peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-rtl@2.0.0-rc.65': - resolution: {integrity: sha512-UfucYEFUboPPt24huks3w8eQ4ihUDWLs8wXeLlxMOT9OaFFf92ZMWJc9xLn/TIv8qFrHZ9AB9/Tg770Y9sqTBw==} + resolution: + { + integrity: sha512-UfucYEFUboPPt24huks3w8eQ4ihUDWLs8wXeLlxMOT9OaFFf92ZMWJc9xLn/TIv8qFrHZ9AB9/Tg770Y9sqTBw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-sass-palette@2.0.0-rc.63': - resolution: {integrity: sha512-aoOOWP0Of016AnpTDjE6IdtQ1zQtoKfO7ONyij4L9kt0JRUuk0IdEgnv9W1q3f0Hw9RkdhH8labGsapLKYSVFA==} + resolution: + { + integrity: sha512-aoOOWP0Of016AnpTDjE6IdtQ1zQtoKfO7ONyij4L9kt0JRUuk0IdEgnv9W1q3f0Hw9RkdhH8labGsapLKYSVFA== + } peerDependencies: sass: ^1.80.3 sass-embedded: ^1.80.3 @@ -2742,7 +4444,10 @@ packages: optional: true '@vuepress/plugin-sass-palette@2.0.0-rc.65': - resolution: {integrity: sha512-BgHpt2VLnWJbD3tn7b6hlwXsOCGseeKDeeV/tetFxDcnQBv2ajMk+RT9Ps/p6fp98OioMGNMeOF1yX7hj+xHaQ==} + resolution: + { + integrity: sha512-BgHpt2VLnWJbD3tn7b6hlwXsOCGseeKDeeV/tetFxDcnQBv2ajMk+RT9Ps/p6fp98OioMGNMeOF1yX7hj+xHaQ== + } peerDependencies: sass: ^1.80.3 sass-embedded: ^1.80.3 @@ -2757,40 +4462,64 @@ packages: optional: true '@vuepress/plugin-seo@2.0.0-rc.65': - resolution: {integrity: sha512-Jo/5ZFgFN8D8cFROvgveCCpAFgdOQPxoXTAYop/xSVRLxpDPX9tNWBOIO/iGW7pApvowIIO/SLlXmBNfujoDRw==} + resolution: + { + integrity: sha512-Jo/5ZFgFN8D8cFROvgveCCpAFgdOQPxoXTAYop/xSVRLxpDPX9tNWBOIO/iGW7pApvowIIO/SLlXmBNfujoDRw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-shiki@2.0.0-rc.65': - resolution: {integrity: sha512-YrXUaos0FvoCPtG+25xgmo0PgEuJ/S6ZlTkzike+8BOsReR4K+/OcdqHyXX5eyHj8nZb0r9X1loz3AJJJiVYjg==} + resolution: + { + integrity: sha512-YrXUaos0FvoCPtG+25xgmo0PgEuJ/S6ZlTkzike+8BOsReR4K+/OcdqHyXX5eyHj8nZb0r9X1loz3AJJJiVYjg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-sitemap@2.0.0-rc.65': - resolution: {integrity: sha512-OPON4CeQvjVRkOVc+N3zJQXsRn+GMKv6+Ve57s4iQwtTPW9bYarGUJBZIwl4rUzicXVHIDSMsMd+Sk0mjHyaSw==} + resolution: + { + integrity: sha512-OPON4CeQvjVRkOVc+N3zJQXsRn+GMKv6+Ve57s4iQwtTPW9bYarGUJBZIwl4rUzicXVHIDSMsMd+Sk0mjHyaSw== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-theme-data@2.0.0-rc.63': - resolution: {integrity: sha512-F8QLiwFOgu9yh1YoJvEHL9uNeMx/agpRFyvCgKckKYgV9iyBN5cIts0hPBv4ADpp1yRWNgkEK3jmAtLUKhxE0g==} + resolution: + { + integrity: sha512-F8QLiwFOgu9yh1YoJvEHL9uNeMx/agpRFyvCgKckKYgV9iyBN5cIts0hPBv4ADpp1yRWNgkEK3jmAtLUKhxE0g== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-theme-data@2.0.0-rc.64': - resolution: {integrity: sha512-9L0raY6ckFNuHyUj5XoxLSPsoseq/3h8XjULEKwYs4J7S+2U+ImBL0C7hDPY8lq+sF6cz6V8mhdZ/e5fs6BiBg==} + resolution: + { + integrity: sha512-9L0raY6ckFNuHyUj5XoxLSPsoseq/3h8XjULEKwYs4J7S+2U+ImBL0C7hDPY8lq+sF6cz6V8mhdZ/e5fs6BiBg== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/plugin-watermark@2.0.0-rc.65': - resolution: {integrity: sha512-zRuRItLhzWOajzKR8tW3sWN5zMxITc5jYlFrCqvGeKDctebvhqN43PMdrlXY5axt7BPLtBwgSGPd/OV3pX0otQ==} + resolution: + { + integrity: sha512-zRuRItLhzWOajzKR8tW3sWN5zMxITc5jYlFrCqvGeKDctebvhqN43PMdrlXY5axt7BPLtBwgSGPd/OV3pX0otQ== + } peerDependencies: vuepress: 2.0.0-rc.18 '@vuepress/shared@2.0.0-rc.18': - resolution: {integrity: sha512-um5/ZKGOKgaui5Xo1aRSZ3ko7zVq60k1M8j8ajEOu90LUD1e8glTa7Km9OIBgPcN+yx2OrNwuu8ieEupdNAm4w==} + resolution: + { + integrity: sha512-um5/ZKGOKgaui5Xo1aRSZ3ko7zVq60k1M8j8ajEOu90LUD1e8glTa7Km9OIBgPcN+yx2OrNwuu8ieEupdNAm4w== + } '@vuepress/theme-default@2.0.0-rc.65': - resolution: {integrity: sha512-PGD4zohq70zzN+oqrneZYB9dHmc+IiSEeItaNjvHhJHRlW4Oc2kSjY/Qho7mXT4dBf+K8mKHHUj9nhQpKs8ulA==} + resolution: + { + integrity: sha512-PGD4zohq70zzN+oqrneZYB9dHmc+IiSEeItaNjvHhJHRlW4Oc2kSjY/Qho7mXT4dBf+K8mKHHUj9nhQpKs8ulA== + } peerDependencies: sass: ^1.80.3 sass-embedded: ^1.80.3 @@ -2805,102 +4534,189 @@ packages: optional: true '@vuepress/utils@2.0.0-rc.18': - resolution: {integrity: sha512-0+5vrv3CBycWpAahmutEK2iyuc9oL6JOWMuAdh+cYuuHt1vX+LHfhWGvSep+UT6pOFGOcZfQzXSdlbkv3b4j+Q==} + resolution: + { + integrity: sha512-0+5vrv3CBycWpAahmutEK2iyuc9oL6JOWMuAdh+cYuuHt1vX+LHfhWGvSep+UT6pOFGOcZfQzXSdlbkv3b4j+Q== + } '@vueuse/core@12.0.0': - resolution: {integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==} + resolution: + { + integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw== + } '@vueuse/metadata@12.0.0': - resolution: {integrity: sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==} + resolution: + { + integrity: sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ== + } '@vueuse/shared@12.0.0': - resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + resolution: + { + integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw== + } '@webassemblyjs/ast@1.12.1': - resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} + resolution: + { + integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + } '@webassemblyjs/floating-point-hex-parser@1.11.6': - resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + resolution: + { + integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + } '@webassemblyjs/helper-api-error@1.11.6': - resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + resolution: + { + integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + } '@webassemblyjs/helper-buffer@1.12.1': - resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} + resolution: + { + integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + } '@webassemblyjs/helper-numbers@1.11.6': - resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + resolution: + { + integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + } '@webassemblyjs/helper-wasm-bytecode@1.11.6': - resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + resolution: + { + integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + } '@webassemblyjs/helper-wasm-section@1.12.1': - resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} + resolution: + { + integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + } '@webassemblyjs/ieee754@1.11.6': - resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + resolution: + { + integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + } '@webassemblyjs/leb128@1.11.6': - resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + resolution: + { + integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + } '@webassemblyjs/utf8@1.11.6': - resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + resolution: + { + integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + } '@webassemblyjs/wasm-edit@1.12.1': - resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} + resolution: + { + integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + } '@webassemblyjs/wasm-gen@1.12.1': - resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} + resolution: + { + integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + } '@webassemblyjs/wasm-opt@1.12.1': - resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} + resolution: + { + integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + } '@webassemblyjs/wasm-parser@1.12.1': - resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} + resolution: + { + integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + } '@webassemblyjs/wast-printer@1.12.1': - resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + resolution: + { + integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + } '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + resolution: + { + integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + } '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + resolution: + { + integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + } JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + resolution: + { + integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + } hasBin: true accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + } + engines: { node: '>= 0.6' } accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + } + engines: { node: '>= 0.6' } acorn-import-assertions@1.9.0: - resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + resolution: + { + integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + } deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + } peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + } + engines: { node: '>=0.4.0' } acorn@8.13.0: - resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + } + engines: { node: '>=0.4.0' } hasBin: true ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + resolution: + { + integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + } peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: @@ -2908,576 +4724,1023 @@ packages: optional: true ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + resolution: + { + integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + } peerDependencies: ajv: ^6.9.1 ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + resolution: + { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + } ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + resolution: + { + integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + } ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + resolution: + { + integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + } ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + } + engines: { node: '>=6' } ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + } + engines: { node: '>=8' } ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + } + engines: { node: '>=18' } ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + } + engines: { node: '>=8' } ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + } + engines: { node: '>=12' } ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + } + engines: { node: '>=4' } ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + } + engines: { node: '>=8' } ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + } + engines: { node: '>=10' } ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + } + engines: { node: '>=12' } anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + } + engines: { node: '>= 8' } append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + resolution: + { + integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + } arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + resolution: + { + integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + } arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + resolution: + { + integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + } argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + resolution: + { + integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + } argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + } aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + resolution: + { + integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + } array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + } + engines: { node: '>= 0.4' } array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + resolution: + { + integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== + } array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + } + engines: { node: '>= 0.4' } array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + } + engines: { node: '>=8' } array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + } + engines: { node: '>= 0.4' } array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + } + engines: { node: '>= 0.4' } array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + } + engines: { node: '>= 0.4' } array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + } + engines: { node: '>= 0.4' } array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + } + engines: { node: '>= 0.4' } arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + } + engines: { node: '>= 0.4' } asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + resolution: + { + integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + } assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + resolution: + { + integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + } assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + } + engines: { node: '>=12' } ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + resolution: + { + integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + } ast-v8-to-istanbul@0.3.10: - resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} + resolution: + { + integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ== + } asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + resolution: + { + integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + } autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== + } + engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: postcss: ^8.1.0 available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + } + engines: { node: '>= 0.4' } axe-core@4.10.0: - resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== + } + engines: { node: '>=4' } axios@1.15.1: - resolution: {integrity: sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==} + resolution: + { + integrity: sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg== + } axios@1.7.7: - resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + resolution: + { + integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + } axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + } + engines: { node: '>= 0.4' } babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: '@babel/core': ^7.8.0 babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + } + engines: { node: '>=8' } babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } babel-preset-current-node-syntax@1.1.0: - resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + resolution: + { + integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + } peerDependencies: '@babel/core': ^7.0.0 babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: '@babel/core': ^7.0.0 balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + } balloon-css@1.2.0: - resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} + resolution: + { + integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A== + } base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + } bcrypt-ts@5.0.3: - resolution: {integrity: sha512-2FcgD12xPbwCoe5i9/HK0jJ1xA1m+QfC1e6htG9Bl/hNOnLyaFmQSlqLKcfe3QdnoMPKpKEGFCbESBTg+SJNOw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-2FcgD12xPbwCoe5i9/HK0jJ1xA1m+QfC1e6htG9Bl/hNOnLyaFmQSlqLKcfe3QdnoMPKpKEGFCbESBTg+SJNOw== + } + engines: { node: '>=18' } binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bindings@1.2.1: - resolution: {integrity: sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==} + resolution: + { + integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + } + engines: { node: '>=8' } birpc@0.2.19: - resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + resolution: + { + integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ== + } bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + resolution: + { + integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + } body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + } + engines: { node: '>=18' } boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + resolution: + { + integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + } brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + resolution: + { + integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + } brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + resolution: + { + integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + } braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + } + engines: { node: '>=8' } browserslist@4.24.0: - resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + resolution: + { + integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + } + engines: { node: '>= 6' } bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + resolution: + { + integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + } buffer-builder@0.2.0: - resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + resolution: + { + integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg== + } buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + } buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - bufferutil@1.1.0: - resolution: {integrity: sha512-bttEVtkre4VYrZH+ciatjApTuac7jLMQXVXzM/ymw82WFREdOrJO496C4Bkh0/FfYoBUSOFaAYF32a5Q8dyiLw==} + resolution: + { + integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + } bufferutil@4.0.9: - resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} - engines: {node: '>=6.14.2'} + resolution: + { + integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + } + engines: { node: '>=6.14.2' } builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + } + engines: { node: '>=6' } busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} + resolution: + { + integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + } + engines: { node: '>=10.16.0' } bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + } + engines: { node: '>= 0.8' } cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + } + engines: { node: '>=8' } cache-content-type@1.0.1: - resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} - engines: {node: '>= 6.0.0'} + resolution: + { + integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + } + engines: { node: '>= 6.0.0' } cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + } + engines: { node: '>=14.16' } cacheable-request@12.0.1: - resolution: {integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg== + } + engines: { node: '>=18' } call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + } + engines: { node: '>= 0.4' } call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + } + engines: { node: '>= 0.4' } call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + } + engines: { node: '>= 0.4' } callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + } + engines: { node: '>=6' } camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + } + engines: { node: '>=6' } camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + } + engines: { node: '>=10' } caniuse-lite@1.0.30001669: - resolution: {integrity: sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==} + resolution: + { + integrity: sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== + } ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + resolution: + { + integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + } chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + } + engines: { node: '>=4' } chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== + } + engines: { node: '>=12' } chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== + } + engines: { node: '>=18' } chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + } + engines: { node: '>=4' } chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + } + engines: { node: '>=10' } chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + resolution: + { + integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + } + engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + } + engines: { node: '>=10' } character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + resolution: + { + integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + } character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + resolution: + { + integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + } chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + resolution: + { + integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + } check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + resolution: + { + integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + } check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} + resolution: + { + integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + } + engines: { node: '>= 16' } cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + resolution: + { + integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + } cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} - engines: {node: '>=18.17'} + resolution: + { + integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + } + engines: { node: '>=18.17' } chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} + resolution: + { + integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + } + engines: { node: '>= 8.10.0' } chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + resolution: + { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + } + engines: { node: '>= 8.10.0' } chokidar@4.0.1: - resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} - engines: {node: '>= 14.16.0'} + resolution: + { + integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + } + engines: { node: '>= 14.16.0' } chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + } + engines: { node: '>=6.0' } ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + } + engines: { node: '>=8' } cjs-module-lexer@1.4.1: - resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + resolution: + { + integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== + } clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw== + } + engines: { node: '>=4' } cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + } + engines: { node: '>=8' } cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + } + engines: { node: '>=18' } cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + } + engines: { node: '>=6' } cli-table3@0.6.3: - resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} - engines: {node: 10.* || >= 12.*} + resolution: + { + integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + } + engines: { node: 10.* || >= 12.* } cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + } + engines: { node: '>=18' } cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} + resolution: + { + integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + } + engines: { node: '>= 10' } cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + resolution: + { + integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + } cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + } + engines: { node: '>=12' } clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + } + engines: { node: '>=0.8' } co-body@6.2.0: - resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA== + } + engines: { node: '>=8.0.0' } co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + resolution: + { + integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + } + engines: { iojs: '>= 1.0.0', node: '>= 0.12.0' } collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + resolution: + { + integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + } color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + resolution: + { + integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + } color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + } + engines: { node: '>=7.0.0' } color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + resolution: + { + integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + } color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + } colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + resolution: + { + integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + } colorjs.io@0.5.2: - resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + resolution: + { + integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw== + } combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + } + engines: { node: '>= 0.8' } comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + resolution: + { + integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + } commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + } + engines: { node: '>=18' } commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + resolution: + { + integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + } commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + } + engines: { node: '>= 6' } commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} + resolution: + { + integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + } + engines: { node: ^12.20.0 || >=14 } commitlint@19.5.0: - resolution: {integrity: sha512-lCtwxgFulvMnCgBc8MVPlVMf+PNOqQSBhHpEnjV2JjEQEAhxjVDtC7IeuEtR+hHpGvHt6CwlLtm3uZNyEby5dQ==} - engines: {node: '>=v18'} + resolution: + { + integrity: sha512-lCtwxgFulvMnCgBc8MVPlVMf+PNOqQSBhHpEnjV2JjEQEAhxjVDtC7IeuEtR+hHpGvHt6CwlLtm3uZNyEby5dQ== + } + engines: { node: '>=v18' } hasBin: true compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + resolution: + { + integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== + } component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + resolution: + { + integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + } computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + resolution: + { + integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q== + } concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + } concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + resolution: + { + integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + } + engines: { '0': node >= 6.0 } confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + resolution: + { + integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + } connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + } + engines: { node: '>=0.8' } consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + resolution: + { + integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + } content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + } + engines: { node: '>= 0.6' } content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + } + engines: { node: '>=18' } content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + } + engines: { node: '>= 0.6' } conventional-changelog-angular@7.0.0: - resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ== + } + engines: { node: '>=16' } conventional-changelog-conventionalcommits@7.0.2: - resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w== + } + engines: { node: '>=16' } conventional-commits-parser@5.0.0: - resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA== + } + engines: { node: '>=16' } hasBin: true convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + } cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} + resolution: + { + integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + } + engines: { node: '>=6.6.0' } cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + } + engines: { node: '>= 0.6' } cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + resolution: + { + integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + } cookies@0.9.1: - resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw== + } + engines: { node: '>= 0.8' } copy-anything@3.0.5: - resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} - engines: {node: '>=12.13'} + resolution: + { + integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + } + engines: { node: '>=12.13' } cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + } + engines: { node: '>= 0.10' } cosmiconfig-typescript-loader@5.0.0: - resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} - engines: {node: '>=v16'} + resolution: + { + integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA== + } + engines: { node: '>=v16' } peerDependencies: '@types/node': '*' cosmiconfig: '>=8.2' typescript: '>=4' cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + } + engines: { node: '>=10' } cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + } + engines: { node: '>=14' } peerDependencies: typescript: '>=4.9.5' peerDependenciesMeta: @@ -3485,63 +5748,111 @@ packages: optional: true create-codepen@2.0.0: - resolution: {integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A== + } + engines: { node: '>=18' } create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } hasBin: true create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + resolution: + { + integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + } cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + resolution: + { + integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + } + engines: { node: '>=10.14', npm: '>=6', yarn: '>=1' } hasBin: true cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + } + engines: { node: '>= 8' } css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + resolution: + { + integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + } css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + } + engines: { node: '>= 6' } csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + resolution: + { + integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + } damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + resolution: + { + integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + } dargs@8.1.0: - resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== + } + engines: { node: '>=12' } data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + } + engines: { node: '>= 0.4' } data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + } + engines: { node: '>= 0.4' } data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + } + engines: { node: '>= 0.4' } dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + resolution: + { + integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + } de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + resolution: + { + integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + } debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + resolution: + { + integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + } peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3549,8 +5860,11 @@ packages: optional: true debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + } + engines: { node: '>=6.0' } peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3558,8 +5872,11 @@ packages: optional: true debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + } + engines: { node: '>=6.0' } peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -3567,15 +5884,24 @@ packages: optional: true decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + } + engines: { node: '>=0.10.0' } decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + } + engines: { node: '>=10' } dedent@1.5.3: - resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + resolution: + { + integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + } peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -3583,316 +5909,564 @@ packages: optional: true deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + } + engines: { node: '>=6' } deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + } + engines: { node: '>=6' } deep-equal@1.0.1: - resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + resolution: + { + integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== + } deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + } + engines: { node: '>= 0.4' } deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + } deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + } + engines: { node: '>=0.10.0' } defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + resolution: + { + integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + } defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + } + engines: { node: '>=10' } define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + } + engines: { node: '>= 0.4' } define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + } + engines: { node: '>=8' } define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + } + engines: { node: '>= 0.4' } delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + } + engines: { node: '>=0.4.0' } delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + resolution: + { + integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + } depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + } + engines: { node: '>= 0.6' } depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + } + engines: { node: '>= 0.8' } dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + } + engines: { node: '>=6' } destr@2.0.3: - resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + resolution: + { + integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + } destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + resolution: + { + integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + } + engines: { node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16 } detect-indent@7.0.1: - resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== + } + engines: { node: '>=12.20' } detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + } + engines: { node: '>=0.10' } hasBin: true detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + } + engines: { node: '>=8' } detect-newline@4.0.1: - resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + resolution: + { + integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + } dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + resolution: + { + integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + } diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} + resolution: + { + integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + } + engines: { node: '>=0.3.1' } dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + resolution: + { + integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + } dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + } + engines: { node: '>=8' } doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + } + engines: { node: '>=0.10.0' } doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + } + engines: { node: '>=6.0.0' } dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + resolution: + { + integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + } domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + resolution: + { + integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + } domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + } + engines: { node: '>= 4' } domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + resolution: + { + integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + } dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + } + engines: { node: '>=8' } dotenv@16.0.3: - resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + } + engines: { node: '>=12' } dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + } + engines: { node: '>= 0.4' } ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + resolution: + { + integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + } electron-to-chromium@1.5.39: - resolution: {integrity: sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==} + resolution: + { + integrity: sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg== + } emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + } + engines: { node: '>=12' } emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + resolution: + { + integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + } emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + resolution: + { + integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + } emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + } emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + resolution: + { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + } encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + } + engines: { node: '>= 0.8' } encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + } + engines: { node: '>= 0.8' } encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + resolution: + { + integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + } end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - endpoint@0.4.5: - resolution: {integrity: sha512-oA2ALUF+d4Y0I8/WMV/0BuAZGHxfIdAygr9ZXP4rfzmp5zpYZmYKHKAbqRQnrE1YGdPhVg4D24CQkyx2qYEoHg==} + resolution: + { + integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + } enhanced-resolve@5.17.1: - resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + } + engines: { node: '>=10.13.0' } entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + resolution: + { + integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + } + engines: { node: '>=0.12' } env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + } + engines: { node: '>=6' } envinfo@7.14.0: - resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + } + engines: { node: '>=4' } hasBin: true environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + } + engines: { node: '>=18' } error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + resolution: + { + integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + } es-abstract@1.23.3: - resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + } + engines: { node: '>= 0.4' } es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + } + engines: { node: '>= 0.4' } es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + } + engines: { node: '>= 0.4' } es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + } + engines: { node: '>= 0.4' } es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + resolution: + { + integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + } es-iterator-helpers@1.1.0: - resolution: {integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== + } + engines: { node: '>= 0.4' } es-module-lexer@1.5.4: - resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + resolution: + { + integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + } es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + resolution: + { + integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + } es-object-atoms@1.0.0: - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + } + engines: { node: '>= 0.4' } es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + } + engines: { node: '>= 0.4' } es-set-tostringtag@2.0.3: - resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + } + engines: { node: '>= 0.4' } es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + } + engines: { node: '>= 0.4' } es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + resolution: + { + integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + } es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + } + engines: { node: '>= 0.4' } esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + } + engines: { node: '>=12' } hasBin: true esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw== + } + engines: { node: '>=18' } + hasBin: true + + esbuild@0.28.2: + resolution: + { + integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + } + engines: { node: '>=18' } hasBin: true escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + } + engines: { node: '>=6' } escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + resolution: + { + integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + } escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} + resolution: + { + integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + } + engines: { node: '>=0.8.0' } escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + } + engines: { node: '>=8' } escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + } + engines: { node: '>=10' } eslint-config-prettier@8.10.0: - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + resolution: + { + integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== + } hasBin: true peerDependencies: eslint: '>=7.0.0' eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + resolution: + { + integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + } hasBin: true peerDependencies: eslint: '>=7.0.0' eslint-config-turbo@1.13.4: - resolution: {integrity: sha512-+we4eWdZlmlEn7LnhXHCIPX/wtujbHCS7XjQM/TN09BHNEl2fZ8id4rHfdfUKIYTSKyy8U/nNyJ0DNoZj5Q8bw==} + resolution: + { + integrity: sha512-+we4eWdZlmlEn7LnhXHCIPX/wtujbHCS7XjQM/TN09BHNEl2fZ8id4rHfdfUKIYTSKyy8U/nNyJ0DNoZj5Q8bw== + } peerDependencies: eslint: '>6.6.0' eslint-import-resolver-alias@1.1.2: - resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w== + } + engines: { node: '>= 4' } peerDependencies: eslint-plugin-import: '>=1.4.0' eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + resolution: + { + integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + } eslint-import-resolver-typescript@3.6.3: - resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} - engines: {node: ^14.18.0 || >=16.0.0} + resolution: + { + integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== + } + engines: { node: ^14.18.0 || >=16.0.0 } peerDependencies: eslint: '*' eslint-plugin-import: '*' @@ -3904,8 +6478,11 @@ packages: optional: true eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== + } + engines: { node: '>=4' } peerDependencies: '@typescript-eslint/parser': '*' eslint: '*' @@ -3925,14 +6502,20 @@ packages: optional: true eslint-plugin-eslint-comments@3.2.0: - resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} - engines: {node: '>=6.5.0'} + resolution: + { + integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ== + } + engines: { node: '>=6.5.0' } peerDependencies: eslint: '>=4.19.1' eslint-plugin-import@2.31.0: - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== + } + engines: { node: '>=4' } peerDependencies: '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 @@ -3941,8 +6524,11 @@ packages: optional: true eslint-plugin-jest@27.9.0: - resolution: {integrity: sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 || ^7.0.0 eslint: ^7.0.0 || ^8.0.0 @@ -3954,17 +6540,26 @@ packages: optional: true eslint-plugin-jsx-a11y@6.10.0: - resolution: {integrity: sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== + } + engines: { node: '>=4.0' } peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 eslint-plugin-only-warn@1.1.0: - resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA== + } + engines: { node: '>=6' } eslint-plugin-playwright@0.16.0: - resolution: {integrity: sha512-DcHpF0SLbNeh9MT4pMzUGuUSnJ7q5MWbP8sSEFIMS6j7Ggnduq8ghNlfhURgty4c1YFny7Ge9xYTO1FSAoV2Vw==} + resolution: + { + integrity: sha512-DcHpF0SLbNeh9MT4pMzUGuUSnJ7q5MWbP8sSEFIMS6j7Ggnduq8ghNlfhURgty4c1YFny7Ge9xYTO1FSAoV2Vw== + } peerDependencies: eslint: '>=7' eslint-plugin-jest: '>=25' @@ -3973,8 +6568,11 @@ packages: optional: true eslint-plugin-prettier@4.2.1: - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + } + engines: { node: '>=12.0.0' } peerDependencies: eslint: '>=7.28.0' eslint-config-prettier: '*' @@ -3984,184 +6582,319 @@ packages: optional: true eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + } + engines: { node: '>=10' } peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 eslint-plugin-react@7.37.1: - resolution: {integrity: sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== + } + engines: { node: '>=4' } peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 eslint-plugin-testing-library@6.3.0: - resolution: {integrity: sha512-GYcEErTt6EGwE0bPDY+4aehfEBpB2gDBFKohir8jlATSUvzStEyzCx8QWB/14xeKc/AwyXkzScSzMHnFojkWrA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} + resolution: + { + integrity: sha512-GYcEErTt6EGwE0bPDY+4aehfEBpB2gDBFKohir8jlATSUvzStEyzCx8QWB/14xeKc/AwyXkzScSzMHnFojkWrA== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6' } peerDependencies: eslint: ^7.5.0 || ^8.0.0 eslint-plugin-tsdoc@0.2.17: - resolution: {integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==} + resolution: + { + integrity: sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA== + } eslint-plugin-turbo@1.13.4: - resolution: {integrity: sha512-82GfMzrewI/DJB92Bbch239GWbGx4j1zvjk1lqb06lxIlMPnVwUHVwPbAnLfyLG3JuhLv9whxGkO/q1CL18JTg==} + resolution: + { + integrity: sha512-82GfMzrewI/DJB92Bbch239GWbGx4j1zvjk1lqb06lxIlMPnVwUHVwPbAnLfyLG3JuhLv9whxGkO/q1CL18JTg== + } peerDependencies: eslint: '>6.6.0' eslint-plugin-unicorn@48.0.1: - resolution: {integrity: sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw== + } + engines: { node: '>=16' } peerDependencies: eslint: '>=8.44.0' eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + } + engines: { node: '>=8.0.0' } eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + } + engines: { node: '>=10' } eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + } + engines: { node: '>=4' } hasBin: true esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + } + engines: { node: '>=0.10' } esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + } + engines: { node: '>=4.0' } estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + } + engines: { node: '>=4.0' } estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + } + engines: { node: '>=4.0' } estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + } estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + } esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + } + engines: { node: '>=0.10.0' } etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + } + engines: { node: '>= 0.6' } eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + resolution: + { + integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + } events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} + resolution: + { + integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + } + engines: { node: '>=0.8.x' } execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + } + engines: { node: '>=10' } execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + } + engines: { node: '>=10' } execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + resolution: + { + integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + } + engines: { node: '>=16.17' } execa@9.5.2: - resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} - engines: {node: ^18.19.0 || >=20.5.0} + resolution: + { + integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q== + } + engines: { node: ^18.19.0 || >=20.5.0 } exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + } + engines: { node: '>= 0.8.0' } expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + } + engines: { node: '>=12.0.0' } expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== + } + engines: { node: '>=12.0.0' } expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + } + engines: { node: '>= 18' } extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + } + engines: { node: '>=0.10.0' } external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + } + engines: { node: '>=4' } fast-check@4.5.3: - resolution: {integrity: sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==} - engines: {node: '>=12.17.0'} + resolution: + { + integrity: sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA== + } + engines: { node: '>=12.17.0' } fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + } fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + resolution: + { + integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + } fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + resolution: + { + integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + } + engines: { node: '>=8.6.0' } fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + } fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + } fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + resolution: + { + integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + } fast-uri@3.0.3: - resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + resolution: + { + integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + } fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + resolution: + { + integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + } fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + resolution: + { + integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + } fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + } + engines: { node: '>=12.0.0' } peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -4169,54 +6902,93 @@ packages: optional: true fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + resolution: + { + integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + } figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + } + engines: { node: '>=8' } figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg== + } + engines: { node: '>=18' } file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + resolution: + { + integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + } + engines: { node: ^10.12.0 || >=12.0.0 } file-type@21.3.0: - resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA== + } + engines: { node: '>=20' } fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + } + engines: { node: '>=8' } finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} + resolution: + { + integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + } + engines: { node: '>= 18.0.0' } find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + } + engines: { node: '>=8' } find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + } + engines: { node: '>=10' } find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g== + } + engines: { node: '>=18' } flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + resolution: + { + integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + } + engines: { node: ^10.12.0 || >=12.0.0 } flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + resolution: + { + integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + } follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + } + engines: { node: '>=4.0' } peerDependencies: debug: '*' peerDependenciesMeta: @@ -4224,8 +6996,11 @@ packages: optional: true follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + } + engines: { node: '>=4.0' } peerDependencies: debug: '*' peerDependenciesMeta: @@ -4233,680 +7008,1210 @@ packages: optional: true for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + resolution: + { + integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + } fork-ts-checker-webpack-plugin@8.0.0: - resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} - engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + resolution: + { + integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg== + } + engines: { node: '>=12.13.0', yarn: '>=1.0.0' } peerDependencies: typescript: '>3.6.0' webpack: ^5.11.0 form-data-encoder@4.0.2: - resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw== + } + engines: { node: '>= 18' } form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + } + engines: { node: '>= 6' } form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + } + engines: { node: '>= 6' } formidable@2.1.2: - resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + resolution: + { + integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + } deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + } + engines: { node: '>= 0.6' } fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + resolution: + { + integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + } fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + } + engines: { node: '>= 0.6' } fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + } + engines: { node: '>= 0.8' } fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + } + engines: { node: '>=12' } fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} + resolution: + { + integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + } + engines: { node: '>=14.14' } fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + resolution: + { + integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + } + engines: { node: '>=6 <7 || >=8' } fs-monkey@1.0.6: - resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + resolution: + { + integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + } fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + } + + 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} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + } function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + } + engines: { node: '>= 0.4' } functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + resolution: + { + integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + } gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + } + engines: { node: '>=6.9.0' } get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + } + engines: { node: 6.* || 8.* || >= 10.* } get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + } + engines: { node: '>=18' } get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + resolution: + { + integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + } get-intrinsic@1.2.4: - resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + } + engines: { node: '>= 0.4' } get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + } + engines: { node: '>= 0.4' } get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + } + engines: { node: '>=8.0.0' } get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + } + engines: { node: '>= 0.4' } get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== + } + engines: { node: '>=12' } get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + } + engines: { node: '>=8' } get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + } + engines: { node: '>=10' } get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + } + engines: { node: '>=16' } get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA== + } + engines: { node: '>=18' } get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + } + engines: { node: '>= 0.4' } get-tsconfig@4.8.1: - resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + resolution: + { + integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + } giscus@1.5.0: - resolution: {integrity: sha512-t3LL0qbSO3JXq3uyQeKpF5CegstGfKX/0gI6eDe1cmnI7D56R7j52yLdzw4pdKrg3VnufwCgCM3FDz7G1Qr6lg==} + resolution: + { + integrity: sha512-t3LL0qbSO3JXq3uyQeKpF5CegstGfKX/0gI6eDe1cmnI7D56R7j52yLdzw4pdKrg3VnufwCgCM3FDz7G1Qr6lg== + } git-hooks-list@3.1.0: - resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} + resolution: + { + integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== + } git-raw-commits@4.0.0: - resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} - engines: {node: '>=16'} - deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. + resolution: + { + integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ== + } + engines: { node: '>=16' } + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + } + engines: { node: '>= 6' } glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + } + engines: { node: '>=10.13.0' } glob-to-regex.js@1.2.0: - resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + resolution: + { + integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + } glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + resolution: + { + integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + } deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + } + engines: { node: '>=16 || 14 >=14.17' } deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + } + engines: { node: '>=18' } globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + } + engines: { node: '>=4' } globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + } + engines: { node: '>=8' } globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + } + engines: { node: '>= 0.4' } globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + } + engines: { node: '>=10' } globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw== + } + engines: { node: '>=18' } gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + resolution: + { + integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + } gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + } + engines: { node: '>= 0.4' } got@14.4.3: - resolution: {integrity: sha512-iTC0Z87yxSijWTh/IpvGpwOhIQK7+GgWkYrMRoN/hB9qeRj9RPuLGODwevs0p5idUf7nrxCVa5IlOmK3b8z+KA==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-iTC0Z87yxSijWTh/IpvGpwOhIQK7+GgWkYrMRoN/hB9qeRj9RPuLGODwevs0p5idUf7nrxCVa5IlOmK3b8z+KA== + } + engines: { node: '>=20' } graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + } graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + resolution: + { + integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + } gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + } + engines: { node: '>=6.0' } has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + resolution: + { + integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + } has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + } + engines: { node: '>=4' } has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + } + engines: { node: '>=8' } has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + resolution: + { + integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + } has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + } + engines: { node: '>= 0.4' } has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + } + engines: { node: '>= 0.4' } has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + } + engines: { node: '>= 0.4' } has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + } + engines: { node: '>= 0.4' } hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + resolution: + { + integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + } hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + } + engines: { node: '>= 0.4' } hast-util-to-html@9.0.3: - resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + resolution: + { + integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== + } hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + resolution: + { + integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + } he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + resolution: + { + integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + } hasBin: true hexoid@1.0.0: - resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + } + engines: { node: '>=8' } hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + resolution: + { + integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + } hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + resolution: + { + integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + } html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + } html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + resolution: + { + integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + } htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + resolution: + { + integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + } http-assert@1.5.0: - resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w== + } + engines: { node: '>= 0.8' } http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + resolution: + { + integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + } http-errors@1.8.1: - resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + } + engines: { node: '>= 0.6' } http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + } + engines: { node: '>= 0.8' } http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + } + engines: { node: '>= 0.8' } http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} + resolution: + { + integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + } + engines: { node: '>=10.19.0' } human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} + resolution: + { + integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + } + engines: { node: '>=8.12.0' } human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} + resolution: + { + integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + } + engines: { node: '>=10.17.0' } human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + resolution: + { + integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + } + engines: { node: '>=16.17.0' } human-signals@8.0.0: - resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} - engines: {node: '>=18.18.0'} + resolution: + { + integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA== + } + engines: { node: '>=18.18.0' } husky@9.1.6: - resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A== + } + engines: { node: '>=18' } hasBin: true hyperdyperid@1.2.0: - resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} - engines: {node: '>=10.18'} + resolution: + { + integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + } + engines: { node: '>=10.18' } iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + } + engines: { node: '>=0.10.0' } iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + } + engines: { node: '>=0.10.0' } iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + } + engines: { node: '>=0.10.0' } ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + } ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + resolution: + { + integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + } ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + } + engines: { node: '>= 4' } immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + resolution: + { + integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== + } immutable@5.0.3: - resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + resolution: + { + integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== + } import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + } + engines: { node: '>=6' } import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + } + engines: { node: '>=8' } import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + } + engines: { node: '>=8' } hasBin: true import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + resolution: + { + integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== + } imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + } + engines: { node: '>=0.8.19' } indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + } + engines: { node: '>=8' } inflation@2.1.0: - resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ== + } + engines: { node: '>= 0.8.0' } inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + } deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + } ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + resolution: + { + integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } inquirer@8.2.4: - resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + } + engines: { node: '>=12.0.0' } inquirer@8.2.5: - resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} - engines: {node: '>=12.0.0'} - - inspector@0.5.0: - resolution: {integrity: sha512-qs91uK2uL3Bjcl+zSe088cR2Jh8TWgjifv54jsIvr593q92rUPFaTo+WZGvoyMgsRZWusZQR6JJmNA913QSZ+A==} + resolution: + { + integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== + } + engines: { node: '>=12.0.0' } internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + } + engines: { node: '>= 0.4' } interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + } + engines: { node: '>= 0.10' } ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + } + engines: { node: '>= 0.10' } is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + } + engines: { node: '>= 0.4' } is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + } + engines: { node: '>= 0.4' } is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + resolution: + { + integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + } is-async-function@2.0.0: - resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + } + engines: { node: '>= 0.4' } is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + resolution: + { + integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + } is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + } + engines: { node: '>=8' } is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + } + engines: { node: '>= 0.4' } is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + } + engines: { node: '>=6' } is-bun-module@1.2.1: - resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} + resolution: + { + integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q== + } is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + } + engines: { node: '>= 0.4' } is-core-module@2.15.1: - resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + } + engines: { node: '>= 0.4' } is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + } + engines: { node: '>= 0.4' } is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + } + engines: { node: '>= 0.4' } is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + } + engines: { node: '>=8' } hasBin: true is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + } + engines: { node: '>=0.10.0' } is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + } + engines: { node: '>=0.10.0' } is-finalizationregistry@1.0.2: - resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + resolution: + { + integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + } is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + } + engines: { node: '>=8' } is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + } + engines: { node: '>=12' } is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + } + engines: { node: '>=18' } is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + } + engines: { node: '>=6' } is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + } + engines: { node: '>= 0.4' } is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + } + engines: { node: '>=0.10.0' } is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + } + engines: { node: '>=8' } is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + } + engines: { node: '>=12' } is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + } + engines: { node: '>= 0.4' } is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + } + engines: { node: '>= 0.4' } is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + } + engines: { node: '>= 0.4' } is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + } + engines: { node: '>=0.12.0' } is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + } + engines: { node: '>=8' } is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + } + engines: { node: '>=8' } is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + } + engines: { node: '>=12' } is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + resolution: + { + integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + } is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + } + engines: { node: '>= 0.4' } is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + } + engines: { node: '>= 0.4' } is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + } + engines: { node: '>= 0.4' } is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + } + engines: { node: '>=8' } is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== + } + engines: { node: '>=18' } is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + } + engines: { node: '>= 0.4' } is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + } + engines: { node: '>= 0.4' } is-text-path@2.0.0: - resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== + } + engines: { node: '>=8' } is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + } + engines: { node: '>= 0.4' } is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + } + engines: { node: '>=10' } is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + } + engines: { node: '>=12' } is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + } + engines: { node: '>=18' } is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + } + engines: { node: '>= 0.4' } is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + resolution: + { + integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + } is-weakset@2.0.3: - resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + } + engines: { node: '>= 0.4' } is-what@4.1.16: - resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} - engines: {node: '>=12.13'} + resolution: + { + integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + } + engines: { node: '>=12.13' } is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + } + engines: { node: '>=8' } isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + resolution: + { + integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + } isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + } istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + } + engines: { node: '>=8' } istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + } + engines: { node: '>=8' } istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + } + engines: { node: '>=10' } istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + } + engines: { node: '>=10' } istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + } + engines: { node: '>=10' } istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + } + engines: { node: '>=8' } istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + } + engines: { node: '>=8' } iterare@1.2.1: - resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + } + engines: { node: '>=6' } iterator.prototype@1.1.3: - resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== + } + engines: { node: '>= 0.4' } jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -4915,8 +8220,11 @@ packages: optional: true jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } peerDependencies: '@types/node': '*' ts-node: '>=9.0.0' @@ -4927,48 +8235,81 @@ packages: optional: true jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + } + engines: { node: '>=6' } peerDependencies: jest-resolve: '*' peerDependenciesMeta: @@ -4976,52 +8317,88 @@ packages: optional: true jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} + resolution: + { + integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + } + engines: { node: '>= 10.13.0' } jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } jest@29.3.1: - resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -5030,516 +8407,936 @@ packages: optional: true jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + resolution: + { + integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + } hasBin: true jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + resolution: + { + integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== + } js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + } js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + resolution: + { + integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== + } js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + resolution: + { + integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + } js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + resolution: + { + integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + } hasBin: true js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + resolution: + { + integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + } hasBin: true jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + resolution: + { + integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + } hasBin: true jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + } + engines: { node: '>=6' } hasBin: true json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + } json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + resolution: + { + integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + } json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + } json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + resolution: + { + integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + } json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + } json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + resolution: + { + integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + } hasBin: true json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + } + engines: { node: '>=6' } hasBin: true jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + resolution: + { + integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + } jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + resolution: + { + integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + } jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + resolution: + { + integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + } jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} + resolution: + { + integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + } + engines: { '0': node >= 0.2.0 } jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + } + engines: { node: '>=4.0' } keygrip@1.1.0: - resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + } + engines: { node: '>= 0.6' } keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + } kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + } + engines: { node: '>=0.10.0' } kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + } + engines: { node: '>=6' } koa-body@6.0.1: - resolution: {integrity: sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg==} + resolution: + { + integrity: sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg== + } koa-compose@4.1.0: - resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + resolution: + { + integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + } koa-convert@2.0.0: - resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} - engines: {node: '>= 10'} + resolution: + { + integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA== + } + engines: { node: '>= 10' } koa-router@12.0.1: - resolution: {integrity: sha512-gaDdj3GtzoLoeosacd50kBBTnnh3B9AYxDThQUo4sfUyXdOhY6ku1qyZKW88tQCRgc3Sw6ChXYXWZwwgjOxE0w==} - engines: {node: '>= 12'} + resolution: + { + integrity: sha512-gaDdj3GtzoLoeosacd50kBBTnnh3B9AYxDThQUo4sfUyXdOhY6ku1qyZKW88tQCRgc3Sw6ChXYXWZwwgjOxE0w== + } + engines: { node: '>= 12' } deprecated: 'Please use @koa/router instead, starting from v9! ' koa-router@13.0.1: - resolution: {integrity: sha512-4/sijXdSxocIe2wv7RFFSxvo2ic1pDzPSmy11yCGztng1hx408qfw1wVmN3aqhQaU7U6nJ039JKC8ObE73Ohgw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-4/sijXdSxocIe2wv7RFFSxvo2ic1pDzPSmy11yCGztng1hx408qfw1wVmN3aqhQaU7U6nJ039JKC8ObE73Ohgw== + } + engines: { node: '>= 18' } deprecated: 'Please use @koa/router instead, starting from v9! ' koa@2.16.4: - resolution: {integrity: sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==} - engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + resolution: + { + integrity: sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw== + } + engines: { node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4 } kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + resolution: + { + integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + } language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + resolution: + { + integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== + } language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + } + engines: { node: '>=0.10' } leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + } + engines: { node: '>=6' } levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + } + engines: { node: '>= 0.8.0' } lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + } + engines: { node: '>=14' } lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + resolution: + { + integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + } linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + resolution: + { + integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + } lint-staged@15.2.10: - resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} - engines: {node: '>=18.12.0'} + resolution: + { + integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg== + } + engines: { node: '>=18.12.0' } hasBin: true listr2@8.2.5: - resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} - engines: {node: '>=18.0.0'} + resolution: + { + integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ== + } + engines: { node: '>=18.0.0' } lit-element@4.1.1: - resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==} + resolution: + { + integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew== + } lit-html@3.2.1: - resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==} + resolution: + { + integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA== + } lit@3.2.1: - resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==} + resolution: + { + integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w== + } load-esm@1.0.3: - resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} - engines: {node: '>=13.2.0'} + resolution: + { + integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA== + } + engines: { node: '>=13.2.0' } loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} - engines: {node: '>=6.11.5'} + resolution: + { + integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + } + engines: { node: '>=6.11.5' } local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + } + engines: { node: '>=14' } locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + } + engines: { node: '>=8' } locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + } + engines: { node: '>=10' } locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + resolution: + { + integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + } lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + resolution: + { + integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + } deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + resolution: + { + integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + } deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + resolution: + { + integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + } lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + resolution: + { + integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== + } lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + resolution: + { + integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + } lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + } lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + resolution: + { + integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + } lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + resolution: + { + integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + } lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + resolution: + { + integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== + } lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + resolution: + { + integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + } lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + resolution: + { + integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== + } lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + resolution: + { + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + } log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + } + engines: { node: '>=10' } log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== + } + engines: { node: '>=18' } log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== + } + engines: { node: '>=18' } loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + resolution: + { + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + } hasBin: true loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + resolution: + { + integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + } loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + resolution: + { + integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== + } lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + resolution: + { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + } lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + } lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + } + engines: { node: '>=10' } macos-release@2.5.1: - resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A== + } + engines: { node: '>=6' } magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== + } + engines: { node: '>=12' } magic-string@0.30.12: - resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + resolution: + { + integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw== + } magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + } magicast@0.5.1: - resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + resolution: + { + integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw== + } make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + } + engines: { node: '>=10' } make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + resolution: + { + integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + } makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + resolution: + { + integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + } markdown-it-anchor@9.2.0: - resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==} + resolution: + { + integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg== + } peerDependencies: '@types/markdown-it': '*' markdown-it: '*' markdown-it-emoji@3.0.0: - resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} + resolution: + { + integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg== + } markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + resolution: + { + integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + } hasBin: true math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + } + engines: { node: '>= 0.4' } mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + resolution: + { + integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== + } mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + resolution: + { + integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + } media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + } + engines: { node: '>= 0.6' } media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + } + engines: { node: '>= 0.8' } medium-zoom@1.1.0: - resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + resolution: + { + integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== + } memfs@3.5.3: - resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw== + } + engines: { node: '>= 4.0.0' } memfs@4.56.10: - resolution: {integrity: sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==} + resolution: + { + integrity: sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w== + } peerDependencies: tslib: '2' meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} + resolution: + { + integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== + } + engines: { node: '>=16.10' } merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + } + engines: { node: '>=18' } merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + resolution: + { + integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + } merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + } + engines: { node: '>= 8' } methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + } + engines: { node: '>= 0.6' } micromark-util-character@2.1.0: - resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + resolution: + { + integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + } micromark-util-encode@2.0.0: - resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + resolution: + { + integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + } micromark-util-sanitize-uri@2.0.0: - resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + resolution: + { + integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + } micromark-util-symbol@2.0.0: - resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + resolution: + { + integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + } micromark-util-types@2.0.0: - resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + resolution: + { + integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + } micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + } + engines: { node: '>=8.6' } mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + } + engines: { node: '>= 0.6' } mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + } + engines: { node: '>= 0.6' } mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + } + engines: { node: '>= 0.6' } mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + } + engines: { node: '>=18' } mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} + resolution: + { + integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + } + engines: { node: '>=4.0.0' } hasBin: true mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + } + engines: { node: '>=6' } mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + } + engines: { node: '>=12' } mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + } + engines: { node: '>=18' } mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + } + engines: { node: '>=10' } mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + } + engines: { node: '>=4' } minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + resolution: + { + integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + } minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + } minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + } + engines: { node: '>=16 || 14 >=14.17' } minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + } + engines: { node: '>=16 || 14 >=14.17' } minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + } + engines: { node: '>=16 || 14 >=14.17' } minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + } minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + } + engines: { node: '>=8' } minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: + { + integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + } + engines: { node: '>=16 || 14 >=14.17' } mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + resolution: + { + integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + } mlly@1.7.2: - resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} + resolution: + { + integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA== + } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + } muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + resolution: + { + integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg== + } multer@2.1.1: - resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} - engines: {node: '>= 10.16.0'} + resolution: + { + integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A== + } + engines: { node: '>= 10.16.0' } mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - - nan@1.8.4: - resolution: {integrity: sha512-609zQ1h3ApgH/94qmbbEklSrjcYYXCHnsWk4MAojq4OUk3tidhDYhPaMasMFKsZPZ96r4eQA1hbR2W4H7/77XA==} + resolution: + { + integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + } nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true nanoid@5.0.9: - resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} - engines: {node: ^18 || >=20} + resolution: + { + integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q== + } + engines: { node: ^18 || >=20 } hasBin: true natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + resolution: + { + integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + } natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + } negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + } + engines: { node: '>= 0.6' } negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + } + engines: { node: '>= 0.6' } neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + resolution: + { + integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + } node-abort-controller@3.1.1: - resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + resolution: + { + integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + } node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + resolution: + { + integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + } node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + resolution: + { + integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + } node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + resolution: + { + integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + } node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} + resolution: + { + integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + } + engines: { node: 4.x || >=6.0.0 } peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: @@ -5547,326 +9344,593 @@ packages: optional: true node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + resolution: + { + integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + } hasBin: true node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + resolution: + { + integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + } node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + resolution: + { + integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + } nodemon@3.1.7: - resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + } + engines: { node: '>=10' } hasBin: true normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + resolution: + { + integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + } normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + } + engines: { node: '>=0.10.0' } normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + } + engines: { node: '>=0.10.0' } normalize-url@8.0.1: - resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + } + engines: { node: '>=14.16' } npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + } + engines: { node: '>=8' } npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA== + } + engines: { node: '>=18' } nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + resolution: + { + integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + } object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + } + engines: { node: '>=0.10.0' } object-inspect@1.13.2: - resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + } + engines: { node: '>= 0.4' } object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + } + engines: { node: '>= 0.4' } object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + } + engines: { node: '>= 0.4' } object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + } + engines: { node: '>= 0.4' } object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + } + engines: { node: '>= 0.4' } object.entries@1.1.8: - resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + } + engines: { node: '>= 0.4' } object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + } + engines: { node: '>= 0.4' } object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + } + engines: { node: '>= 0.4' } object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + } + engines: { node: '>= 0.4' } obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + resolution: + { + integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== + } ofetch@1.4.1: - resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + resolution: + { + integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw== + } on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + } + engines: { node: '>= 0.8' } once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + } onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + } + engines: { node: '>=6' } onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + } + engines: { node: '>=12' } onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + } + engines: { node: '>=18' } oniguruma-to-es@0.7.0: - resolution: {integrity: sha512-HRaRh09cE0gRS3+wi2zxekB+I5L8C/gN60S+vb11eADHUaB/q4u8wGGOX3GvwvitG8ixaeycZfeoyruKQzUgNg==} + resolution: + { + integrity: sha512-HRaRh09cE0gRS3+wi2zxekB+I5L8C/gN60S+vb11eADHUaB/q4u8wGGOX3GvwvitG8ixaeycZfeoyruKQzUgNg== + } only@0.0.2: - resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + resolution: + { + integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== + } open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + } + engines: { node: '>=12' } optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - options@0.0.6: - resolution: {integrity: sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + } + engines: { node: '>= 0.8.0' } ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + } + engines: { node: '>=10' } ora@8.1.0: - resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ== + } + engines: { node: '>=18' } os-name@4.0.1: - resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== + } + engines: { node: '>=10' } os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + } + engines: { node: '>=0.10.0' } p-cancelable@4.0.1: - resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg== + } + engines: { node: '>=14.16' } p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + } + engines: { node: '>=6' } p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + } + engines: { node: '>=10' } p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + } + engines: { node: '>=18' } p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + } + engines: { node: '>=8' } p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + } + engines: { node: '>=10' } p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + } + engines: { node: '>=6' } parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + } + engines: { node: '>=6' } parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + } + engines: { node: '>=8' } parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw== + } + engines: { node: '>=18' } parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + resolution: + { + integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== + } parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + resolution: + { + integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + } parse5@7.2.0: - resolution: {integrity: sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==} + resolution: + { + integrity: sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA== + } parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + } + engines: { node: '>= 0.8' } path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + resolution: + { + integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + } path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + } + engines: { node: '>=8' } path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + resolution: + { + integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + } + engines: { node: '>=0.10.0' } path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + } + engines: { node: '>=8' } path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + } + engines: { node: '>=12' } path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + } path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + resolution: + { + integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + } + engines: { node: '>=16 || 14 >=14.18' } path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + resolution: + { + integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== + } path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + resolution: + { + integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + } path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + } + engines: { node: '>=16' } path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + resolution: + { + integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + } path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + } + engines: { node: '>=8' } path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + } + engines: { node: '>=12' } pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + resolution: + { + integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + } pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + } pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + resolution: + { + integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + } pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} + resolution: + { + integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + } + engines: { node: '>= 14.16' } perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + resolution: + { + integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + } photoswipe@5.4.4: - resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} - engines: {node: '>= 0.12.0'} + resolution: + { + integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA== + } + engines: { node: '>= 0.12.0' } picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + resolution: + { + integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + } picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + } + engines: { node: '>=8.6' } picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + } + engines: { node: '>=12' } pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + } + engines: { node: '>=0.10' } hasBin: true pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + } + engines: { node: '>= 6' } pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + } + engines: { node: '>=8' } pkg-types@1.2.1: - resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + resolution: + { + integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw== + } + + playwright-core@1.62.1: + resolution: + { + integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw== + } + engines: { node: '>=20' } + hasBin: true + + playwright@1.62.1: + resolution: + { + integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg== + } + engines: { node: '>=20' } + hasBin: true pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + } + engines: { node: '>=4' } pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + } + engines: { node: '>=10.13.0' } possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + } + engines: { node: '>= 0.4' } postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g== + } + engines: { node: '>= 18' } peerDependencies: jiti: '>=1.21.0' postcss: '>=8.0.9' @@ -5883,30 +9947,51 @@ packages: optional: true postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + resolution: + { + integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + } postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + } + engines: { node: ^10 || ^12 || >=14 } postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + } + engines: { node: ^10 || ^12 || >=14 } postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + } + engines: { node: ^10 || ^12 || >=14 } prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + } + engines: { node: '>= 0.8.0' } prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + } + engines: { node: '>=6.0.0' } prettier-plugin-packagejson@2.5.3: - resolution: {integrity: sha512-ATMEEXr+ywls1kgrZEWl4SBPEm0uDdyDAjyNzUC0/Z8WZTD3RqbJcQDR+Dau+wYkW9KHK6zqQIsFyfn+9aduWg==} + resolution: + { + integrity: sha512-ATMEEXr+ywls1kgrZEWl4SBPEm0uDdyDAjyNzUC0/Z8WZTD3RqbJcQDR+Dau+wYkW9KHK6zqQIsFyfn+9aduWg== + } peerDependencies: prettier: '>= 1.16.0' peerDependenciesMeta: @@ -5914,767 +9999,1334 @@ packages: optional: true prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + } + engines: { node: '>=10.13.0' } hasBin: true prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + } + engines: { node: '>=14' } hasBin: true pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== + } + engines: { node: '>=18' } prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + } + engines: { node: '>=6' } prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + } + engines: { node: '>= 6' } prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + resolution: + { + integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + } property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + resolution: + { + integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + } proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + } + engines: { node: '>= 0.10' } proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + resolution: + { + integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + } proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== + } + engines: { node: '>=10' } pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + resolution: + { + integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + } pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + resolution: + { + integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + } punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + } + engines: { node: '>=6' } punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + } + engines: { node: '>=6' } pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + resolution: + { + integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + } pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + resolution: + { + integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== + } qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + } + engines: { node: '>=10.13.0' } hasBin: true qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + } + engines: { node: '>=0.6' } qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg== + } + engines: { node: '>=0.6' } queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + } quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + } + engines: { node: '>=10' } randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + resolution: + { + integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + } range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + } + engines: { node: '>= 0.6' } raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + } + engines: { node: '>= 0.8' } raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + } + engines: { node: '>= 0.10' } react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + resolution: + { + integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + } react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + resolution: + { + integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + } read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + } + engines: { node: '>=8' } read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + } + engines: { node: '>=8' } readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + } + engines: { node: '>= 6' } readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + resolution: + { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + } + engines: { node: '>=8.10.0' } readdirp@4.0.2: - resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} - engines: {node: '>= 14.16.0'} + resolution: + { + integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + } + engines: { node: '>= 14.16.0' } rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + } + engines: { node: '>= 0.10' } reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + resolution: + { + integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + } reflect.getprototypeof@1.0.6: - resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + } + engines: { node: '>= 0.4' } regex-recursion@4.3.0: - resolution: {integrity: sha512-5LcLnizwjcQ2ALfOj95MjcatxyqF5RPySx9yT+PaXu3Gox2vyAtLDjHB8NTJLtMGkvyau6nI3CfpwFCjPUIs/A==} + resolution: + { + integrity: sha512-5LcLnizwjcQ2ALfOj95MjcatxyqF5RPySx9yT+PaXu3Gox2vyAtLDjHB8NTJLtMGkvyau6nI3CfpwFCjPUIs/A== + } regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + resolution: + { + integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + } regex@5.0.2: - resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} + resolution: + { + integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ== + } regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + resolution: + { + integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + } hasBin: true regexp.prototype.flags@1.5.3: - resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== + } + engines: { node: '>= 0.4' } regjsparser@0.10.0: - resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + resolution: + { + integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA== + } hasBin: true require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + } + engines: { node: '>=0.10.0' } require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + } + engines: { node: '>=0.10.0' } require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolution: + { + integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + } resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolution: + { + integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + } resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + } + engines: { node: '>=8' } resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + } + engines: { node: '>=4' } resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + } + engines: { node: '>=8' } resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolution: + { + integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + } resolve.exports@2.0.2: - resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + } + engines: { node: '>=10' } resolve@1.19.0: - resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} + resolution: + { + integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + } resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + resolution: + { + integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + } hasBin: true resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + resolution: + { + integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + } hasBin: true responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + } + engines: { node: '>=14.16' } restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + } + engines: { node: '>=8' } restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + } + engines: { node: '>=18' } reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolution: + { + integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + } + engines: { iojs: '>=1.0.0', node: '>=0.10.0' } rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + resolution: + { + integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + } rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + resolution: + { + integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + } deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@4.4.1: - resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== + } + engines: { node: '>=14' } hasBin: true rollup@4.24.0: - resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg== + } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true rollup@4.57.0: - resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA== + } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + } + engines: { node: '>= 18' } run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} + resolution: + { + integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + } + engines: { node: '>=0.12.0' } run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + } rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + resolution: + { + integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + } safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} + resolution: + { + integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + } + engines: { node: '>=0.4' } safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + } safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + } + engines: { node: '>= 0.4' } safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + } sass-embedded-android-arm64@1.82.0: - resolution: {integrity: sha512-bldHMs02QQWXsgHUZRgolNnZdMjN6XHvmUYoRkzmFq7lsvtLU6SJg2S1Wa9IZJs9jRWdTmOgA6YibSf3pROyFQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-bldHMs02QQWXsgHUZRgolNnZdMjN6XHvmUYoRkzmFq7lsvtLU6SJg2S1Wa9IZJs9jRWdTmOgA6YibSf3pROyFQ== + } + engines: { node: '>=14.0.0' } cpu: [arm64] os: [android] sass-embedded-android-arm@1.82.0: - resolution: {integrity: sha512-ttGMvWnA/5TYdZTjr5fWHDbb9nZgKipHKCc9zZQRF5HjUydOYWKNqmAJHQtbFWaq35kd5qn6yiE73IJN6eJ6wA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-ttGMvWnA/5TYdZTjr5fWHDbb9nZgKipHKCc9zZQRF5HjUydOYWKNqmAJHQtbFWaq35kd5qn6yiE73IJN6eJ6wA== + } + engines: { node: '>=14.0.0' } cpu: [arm] os: [android] sass-embedded-android-ia32@1.82.0: - resolution: {integrity: sha512-FUJOnxw8IYKuYuxxiOkk6QXle8/yQFtKjnuSAJuZ5ZpLVMcSZzLc3SWOtuEXYx5iSAfJCO075o2ZoG/pPrJ9aw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-FUJOnxw8IYKuYuxxiOkk6QXle8/yQFtKjnuSAJuZ5ZpLVMcSZzLc3SWOtuEXYx5iSAfJCO075o2ZoG/pPrJ9aw== + } + engines: { node: '>=14.0.0' } cpu: [ia32] os: [android] sass-embedded-android-riscv64@1.82.0: - resolution: {integrity: sha512-rd+vc+sxJxNnbhaubiIJmnb1b3FvC9wxCIq8spstopbO7o1uufvBBDeRoFSJaN+7oNhamzjlYGdu6aQoQNs3+A==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-rd+vc+sxJxNnbhaubiIJmnb1b3FvC9wxCIq8spstopbO7o1uufvBBDeRoFSJaN+7oNhamzjlYGdu6aQoQNs3+A== + } + engines: { node: '>=14.0.0' } cpu: [riscv64] os: [android] sass-embedded-android-x64@1.82.0: - resolution: {integrity: sha512-EVlybGTgJ8wNLyWj8RUatPXSnmIcvCsx3EfsRfBfhGihLbn4NNpavYO9QsvZzI2XWbJqHLBCd+CvkTcDw/TaSQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-EVlybGTgJ8wNLyWj8RUatPXSnmIcvCsx3EfsRfBfhGihLbn4NNpavYO9QsvZzI2XWbJqHLBCd+CvkTcDw/TaSQ== + } + engines: { node: '>=14.0.0' } cpu: [x64] os: [android] sass-embedded-darwin-arm64@1.82.0: - resolution: {integrity: sha512-LvdJPojjKlNGYOB0nSUR/ZtMDuAF4puspHlwK42aA/qK292bfSkMUKZPPapB2aSRwccc/ieBq5fI7n/WHrOCVw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-LvdJPojjKlNGYOB0nSUR/ZtMDuAF4puspHlwK42aA/qK292bfSkMUKZPPapB2aSRwccc/ieBq5fI7n/WHrOCVw== + } + engines: { node: '>=14.0.0' } cpu: [arm64] os: [darwin] sass-embedded-darwin-x64@1.82.0: - resolution: {integrity: sha512-6LfnD6YmG1aBfd3ReqMOJDb6Pg2Z/hmlJB7nU+Lb3E+hCNjAZAgeUHQxU/Pm1eIqJJTU/h4ib5QP0Pt9O8yVnw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-6LfnD6YmG1aBfd3ReqMOJDb6Pg2Z/hmlJB7nU+Lb3E+hCNjAZAgeUHQxU/Pm1eIqJJTU/h4ib5QP0Pt9O8yVnw== + } + engines: { node: '>=14.0.0' } cpu: [x64] os: [darwin] sass-embedded-linux-arm64@1.82.0: - resolution: {integrity: sha512-590/y0HJr/JiyxaqgR7Xf9P20BIhJ+zhB/afAnVuZe/4lEfCpTyM5xMe2+sKLsqtrVyzs9Zm/M4S4ASUOPCggA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-590/y0HJr/JiyxaqgR7Xf9P20BIhJ+zhB/afAnVuZe/4lEfCpTyM5xMe2+sKLsqtrVyzs9Zm/M4S4ASUOPCggA== + } + engines: { node: '>=14.0.0' } cpu: [arm64] os: [linux] sass-embedded-linux-arm@1.82.0: - resolution: {integrity: sha512-ozjdC5rWzyi5Vo300I4tVZzneXOTQUiaxOr7DjtN26HuFaGAGCGmvThh2BRV4RvySg++5H9rdFu+VgyUQ5iukw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-ozjdC5rWzyi5Vo300I4tVZzneXOTQUiaxOr7DjtN26HuFaGAGCGmvThh2BRV4RvySg++5H9rdFu+VgyUQ5iukw== + } + engines: { node: '>=14.0.0' } cpu: [arm] os: [linux] sass-embedded-linux-ia32@1.82.0: - resolution: {integrity: sha512-hpc4acZ3UTjjJ3Q/GUXqQOCSml6AFKaku0HMawra9bKyRmOpxn8V5hqgXeOWVjK2oQzCmCnJvwKoQUP+S/SIYQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-hpc4acZ3UTjjJ3Q/GUXqQOCSml6AFKaku0HMawra9bKyRmOpxn8V5hqgXeOWVjK2oQzCmCnJvwKoQUP+S/SIYQ== + } + engines: { node: '>=14.0.0' } cpu: [ia32] os: [linux] sass-embedded-linux-musl-arm64@1.82.0: - resolution: {integrity: sha512-bc2MUSMv/jabnNGEyKP2jQAYZoEzTT/c633W6QoeSEWETGCuTNjaHvWWE6qSI6/UfRg1EpuV1LQA2jPMzZfv/w==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-bc2MUSMv/jabnNGEyKP2jQAYZoEzTT/c633W6QoeSEWETGCuTNjaHvWWE6qSI6/UfRg1EpuV1LQA2jPMzZfv/w== + } + engines: { node: '>=14.0.0' } cpu: [arm64] os: [linux] sass-embedded-linux-musl-arm@1.82.0: - resolution: {integrity: sha512-R5PQmY/I+GSoMtfLo8GgHkvF/q6x6y8VNM7yu/Ac1mJj86n48VFi29W1HfY2496+Q6cpAq7toobDj7YfldIdVA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-R5PQmY/I+GSoMtfLo8GgHkvF/q6x6y8VNM7yu/Ac1mJj86n48VFi29W1HfY2496+Q6cpAq7toobDj7YfldIdVA== + } + engines: { node: '>=14.0.0' } cpu: [arm] os: [linux] sass-embedded-linux-musl-ia32@1.82.0: - resolution: {integrity: sha512-ZQKCFKm5TBcJ19UG6uUQmIKfVCJIWMb7e1a93lGeujSb9gyKF5Fb6MN3tuExoT7iFK8zU0Z9iyHqh93F58lcCw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-ZQKCFKm5TBcJ19UG6uUQmIKfVCJIWMb7e1a93lGeujSb9gyKF5Fb6MN3tuExoT7iFK8zU0Z9iyHqh93F58lcCw== + } + engines: { node: '>=14.0.0' } cpu: [ia32] os: [linux] sass-embedded-linux-musl-riscv64@1.82.0: - resolution: {integrity: sha512-5meSU8BHFeaT09RWfkuUrikRlC+WZcYb9To7MpfV1d9nlD7CZ2xydPExK+mj3DqRuQvTbvhMPcr7f+pHlgHINQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-5meSU8BHFeaT09RWfkuUrikRlC+WZcYb9To7MpfV1d9nlD7CZ2xydPExK+mj3DqRuQvTbvhMPcr7f+pHlgHINQ== + } + engines: { node: '>=14.0.0' } cpu: [riscv64] os: [linux] sass-embedded-linux-musl-x64@1.82.0: - resolution: {integrity: sha512-ASLAMfjWv7YEPBvEOVlb3zzHq8l4Y9Eh4x3m7B1dNauGVbO11Yng5cPCX/XbwGVf30BtE75pwqvV7oXxBtN15w==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-ASLAMfjWv7YEPBvEOVlb3zzHq8l4Y9Eh4x3m7B1dNauGVbO11Yng5cPCX/XbwGVf30BtE75pwqvV7oXxBtN15w== + } + engines: { node: '>=14.0.0' } cpu: [x64] os: [linux] sass-embedded-linux-riscv64@1.82.0: - resolution: {integrity: sha512-qWvRDXCXH3GzD8OcP0ntd8gBTK3kZyUeyXmxQDZyEtMAM4STC2Tn7+5+2JYYHlppzqWnZPFBNESvpKeOtHaBBw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-qWvRDXCXH3GzD8OcP0ntd8gBTK3kZyUeyXmxQDZyEtMAM4STC2Tn7+5+2JYYHlppzqWnZPFBNESvpKeOtHaBBw== + } + engines: { node: '>=14.0.0' } cpu: [riscv64] os: [linux] sass-embedded-linux-x64@1.82.0: - resolution: {integrity: sha512-AmRaHqShztwfep+M4NagdGaY7fTyWGSOM3k4Z/dd7q4nZclXbALLqNJtKx8xOM7A41LHYJ9zDpIBVRkrh0PzTA==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-AmRaHqShztwfep+M4NagdGaY7fTyWGSOM3k4Z/dd7q4nZclXbALLqNJtKx8xOM7A41LHYJ9zDpIBVRkrh0PzTA== + } + engines: { node: '>=14.0.0' } cpu: [x64] os: [linux] sass-embedded-win32-arm64@1.82.0: - resolution: {integrity: sha512-zL9JDQZHXHSGAZe5DqSrR86wMHbm9QPziU4/3hoIG+99StuS74CuV42+hw/+FXXBkXMWbjKWsyF/HZt+I/wJuw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-zL9JDQZHXHSGAZe5DqSrR86wMHbm9QPziU4/3hoIG+99StuS74CuV42+hw/+FXXBkXMWbjKWsyF/HZt+I/wJuw== + } + engines: { node: '>=14.0.0' } cpu: [arm64] os: [win32] sass-embedded-win32-ia32@1.82.0: - resolution: {integrity: sha512-xE+AzLquCkFPnnpo0NHjQdLRIhG1bVs42xIKx42aUbVLYKkBDvbBGpw6EtTscRMyvcjoOqGH5saRvSFComUQcw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-xE+AzLquCkFPnnpo0NHjQdLRIhG1bVs42xIKx42aUbVLYKkBDvbBGpw6EtTscRMyvcjoOqGH5saRvSFComUQcw== + } + engines: { node: '>=14.0.0' } cpu: [ia32] os: [win32] sass-embedded-win32-x64@1.82.0: - resolution: {integrity: sha512-cEgfOQG5womOzzk16ReTv2dxPq5BG16LgLUold/LH9IZH86u4E/MN7Fspf4RWeEJ2EcLdew9QYSC2YWs1l98dQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-cEgfOQG5womOzzk16ReTv2dxPq5BG16LgLUold/LH9IZH86u4E/MN7Fspf4RWeEJ2EcLdew9QYSC2YWs1l98dQ== + } + engines: { node: '>=14.0.0' } cpu: [x64] os: [win32] sass-embedded@1.82.0: - resolution: {integrity: sha512-v13sRVVZtWAQLpAGTz5D8hy+oyNKRHao5tKVc/P6AMqSP+jDM8X6GkEpL0jfbu3MaN2/hAQsd4Qx14GG1u0prQ==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-v13sRVVZtWAQLpAGTz5D8hy+oyNKRHao5tKVc/P6AMqSP+jDM8X6GkEpL0jfbu3MaN2/hAQsd4Qx14GG1u0prQ== + } + engines: { node: '>=16.0.0' } hasBin: true sass@1.79.5: - resolution: {integrity: sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g== + } + engines: { node: '>=14.0.0' } hasBin: true sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + resolution: + { + integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + } schema-utils@3.3.0: - resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} - engines: {node: '>= 10.13.0'} + resolution: + { + integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + } + engines: { node: '>= 10.13.0' } section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + } + engines: { node: '>=4' } semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + resolution: + { + integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + } hasBin: true semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + } hasBin: true semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + } + engines: { node: '>=10' } hasBin: true semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + } + engines: { node: '>=10' } hasBin: true send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + } + engines: { node: '>= 18' } serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + resolution: + { + integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + } serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} + resolution: + { + integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + } + engines: { node: '>= 18' } set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + resolution: + { + integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + } set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + } + engines: { node: '>= 0.4' } set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + } + engines: { node: '>= 0.4' } setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + resolution: + { + integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + } shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + } + engines: { node: '>=8' } shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + } + engines: { node: '>=8' } shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + } + engines: { node: '>=4' } hasBin: true shiki@1.24.1: - resolution: {integrity: sha512-/qByWMg05+POb63c/OvnrU17FcCUa34WU4F6FCrd/mjDPEDPl8YUNRkRMbo8l3iYMLydfCgxi1r37JFoSw8A4A==} + resolution: + { + integrity: sha512-/qByWMg05+POb63c/OvnrU17FcCUa34WU4F6FCrd/mjDPEDPl8YUNRkRMbo8l3iYMLydfCgxi1r37JFoSw8A4A== + } side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + } + engines: { node: '>= 0.4' } side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + } + engines: { node: '>= 0.4' } side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + } + engines: { node: '>= 0.4' } side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + } + engines: { node: '>= 0.4' } side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + } + engines: { node: '>= 0.4' } siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + } signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + resolution: + { + integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + } signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + } + engines: { node: '>=14' } simple-update-notifier@2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + } + engines: { node: '>=10' } sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + } sitemap@8.0.0: - resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} - engines: {node: '>=14.0.0', npm: '>=6.0.0'} + resolution: + { + integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A== + } + engines: { node: '>=14.0.0', npm: '>=6.0.0' } deprecated: 'SECURITY: Multiple vulnerabilities fixed in 8.0.1 (XML injection, path traversal, command injection, protocol injection). Upgrade immediately: npm install sitemap@8.0.1' hasBin: true slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + } + engines: { node: '>=8' } slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + } + engines: { node: '>=12' } slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + } + engines: { node: '>=14.16' } slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + } + engines: { node: '>=12' } slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + } + engines: { node: '>=18' } sort-object-keys@1.1.3: - resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + resolution: + { + integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== + } sort-package-json@2.10.1: - resolution: {integrity: sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w==} + resolution: + { + integrity: sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w== + } hasBin: true source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + } + engines: { node: '>=0.10.0' } source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + resolution: + { + integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + } source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + resolution: + { + integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + } source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + } + engines: { node: '>=0.10.0' } source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + } + engines: { node: '>= 8' } space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + resolution: + { + integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + } spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + resolution: + { + integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + } spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + resolution: + { + integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + } spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + resolution: + { + integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + } spdx-license-ids@3.0.20: - resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + resolution: + { + integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== + } speakingurl@14.0.1: - resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + } + engines: { node: '>=0.10.0' } split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} + resolution: + { + integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + } + engines: { node: '>= 10.x' } sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + resolution: + { + integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + } stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + } + engines: { node: '>=10' } stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + } statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + } + engines: { node: '>= 0.6' } statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + } + engines: { node: '>= 0.8' } statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + } + engines: { node: '>= 0.8' } std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + resolution: + { + integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + } std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + resolution: + { + integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + } stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== + } + engines: { node: '>=18' } stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + } + engines: { node: '>= 0.4' } streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} + resolution: + { + integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + } + engines: { node: '>=10.0.0' } string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} + resolution: + { + integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + } + engines: { node: '>=0.6.19' } string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + } + engines: { node: '>=10' } string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + } + engines: { node: '>=8' } string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + } + engines: { node: '>=18' } string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== + } + engines: { node: '>= 0.4' } string.prototype.matchall@4.0.11: - resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + } + engines: { node: '>= 0.4' } string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + resolution: + { + integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + } string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + } + engines: { node: '>= 0.4' } string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + resolution: + { + integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + } string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + } + engines: { node: '>= 0.4' } string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + } stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + resolution: + { + integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + } strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + } + engines: { node: '>=8' } strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + } + engines: { node: '>=12' } strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + } + engines: { node: '>=0.10.0' } strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + } + engines: { node: '>=4' } strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + } + engines: { node: '>=8' } strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + } + engines: { node: '>=6' } strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + } + engines: { node: '>=12' } strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== + } + engines: { node: '>=18' } strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + } + engines: { node: '>=8' } strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + } + engines: { node: '>=8' } strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + resolution: + { + integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== + } strtok3@10.3.4: - resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg== + } + engines: { node: '>=18' } superagent@8.1.2: - resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} - engines: {node: '>=6.4.0 <13 || >=14'} + resolution: + { + integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + } + engines: { node: '>=6.4.0 <13 || >=14' } deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superjson@2.2.1: - resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA== + } + engines: { node: '>=16' } supertest@6.3.4: - resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} - engines: {node: '>=6.4.0'} + resolution: + { + integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw== + } + engines: { node: '>=6.4.0' } deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + } + engines: { node: '>=4' } supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + } + engines: { node: '>=8' } supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + } + engines: { node: '>=10' } supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + } + engines: { node: '>= 0.4' } symbol-observable@4.0.0: - resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + } + engines: { node: '>=0.10' } sync-child-process@1.0.2: - resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA== + } + engines: { node: '>=16.0.0' } sync-message-port@1.1.3: - resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg== + } + engines: { node: '>=16.0.0' } synckit@0.9.2: - resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} - engines: {node: ^14.18.0 || >=16.0.0} + resolution: + { + integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== + } + engines: { node: ^14.18.0 || >=16.0.0 } tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + } + engines: { node: '>=6' } terser-webpack-plugin@5.3.10: - resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} - engines: {node: '>= 10.13.0'} + resolution: + { + integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + } + engines: { node: '>= 10.13.0' } peerDependencies: '@swc/core': '*' esbuild: '*' @@ -6689,120 +11341,207 @@ packages: optional: true terser@5.34.1: - resolution: {integrity: sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== + } + engines: { node: '>=10' } hasBin: true test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + } + engines: { node: '>=8' } text-extensions@2.4.0: - resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== + } + engines: { node: '>=8' } text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + resolution: + { + integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + } thingies@2.5.0: - resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} - engines: {node: '>=10.18'} + resolution: + { + integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== + } + engines: { node: '>=10.18' } peerDependencies: tslib: ^2 through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + resolution: + { + integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + } tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + } tinyexec@0.3.1: - resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + resolution: + { + integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== + } tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== + } + engines: { node: '>=18' } tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + } + engines: { node: '>=12.0.0' } tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== + } + engines: { node: '>=14.0.0' } tinypool@1.0.1: - resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA== + } + engines: { node: ^18.0.0 || >=20.0.0 } tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + } + engines: { node: '>=14.0.0' } tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== + } + engines: { node: '>=14.0.0' } tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== + } + engines: { node: '>=14.0.0' } tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + } + engines: { node: '>=14.0.0' } tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + resolution: + { + integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + } + engines: { node: '>=0.6.0' } tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + resolution: + { + integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + } to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + } + engines: { node: '>=4' } to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + } + engines: { node: '>=8.0' } toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + } + engines: { node: '>=0.6' } token-types@6.1.2: - resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} - engines: {node: '>=14.16'} + resolution: + { + integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww== + } + engines: { node: '>=14.16' } touch@3.1.1: - resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + resolution: + { + integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + } hasBin: true tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + resolution: + { + integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + } tree-dump@1.1.0: - resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} - engines: {node: '>=10.0'} + resolution: + { + integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + } + engines: { node: '>=10.0' } peerDependencies: tslib: '2' tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + resolution: + { + integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + } hasBin: true trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + resolution: + { + integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + } ts-api-utils@1.3.0: - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + } + engines: { node: '>=16' } peerDependencies: typescript: '>=4.2.0' ts-jest@29.0.3: - resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + resolution: + { + integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ== + } + engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' @@ -6822,14 +11561,20 @@ packages: optional: true ts-loader@9.5.1: - resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== + } + engines: { node: '>=12.0.0' } peerDependencies: typescript: '*' webpack: ^5.0.0 ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + resolution: + { + integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + } hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -6843,278 +11588,487 @@ packages: optional: true tsconfig-paths-webpack-plugin@4.0.1: - resolution: {integrity: sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw== + } + engines: { node: '>=10.13.0' } tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + resolution: + { + integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + } tsconfig-paths@4.1.1: - resolution: {integrity: sha512-VgPrtLKpRgEAJsMj5Q/I/mXouC6A/7eJ/X4Nuk6o0cRPwBtznYxTCU4FodbexbzH9somBPEXYi0ZkUViUpJ21Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-VgPrtLKpRgEAJsMj5Q/I/mXouC6A/7eJ/X4Nuk6o0cRPwBtznYxTCU4FodbexbzH9somBPEXYi0ZkUViUpJ21Q== + } + engines: { node: '>=6' } tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + } + engines: { node: '>=6' } tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + resolution: + { + integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + } tslib@2.5.3: - resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + resolution: + { + integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== + } tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + resolution: + { + integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + } tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + } tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} + resolution: + { + integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + } + engines: { node: '>=0.6.x' } tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + } + engines: { node: '>= 6' } peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsx@4.23.12: + resolution: + { + integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q== + } + engines: { node: '>=18.0.0' } + hasBin: true + turbo-darwin-64@1.13.4: - resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==} + resolution: + { + integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw== + } cpu: [x64] os: [darwin] turbo-darwin-arm64@1.13.4: - resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==} + resolution: + { + integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA== + } cpu: [arm64] os: [darwin] turbo-linux-64@1.13.4: - resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==} + resolution: + { + integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ== + } cpu: [x64] os: [linux] turbo-linux-arm64@1.13.4: - resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==} + resolution: + { + integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg== + } cpu: [arm64] os: [linux] turbo-windows-64@1.13.4: - resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==} + resolution: + { + integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw== + } cpu: [x64] os: [win32] turbo-windows-arm64@1.13.4: - resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==} + resolution: + { + integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g== + } cpu: [arm64] os: [win32] turbo@1.13.4: - resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==} + resolution: + { + integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ== + } hasBin: true type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + } + engines: { node: '>= 0.8.0' } type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + } + engines: { node: '>=4' } type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + } + engines: { node: '>=4' } type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + } + engines: { node: '>=10' } type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + } + engines: { node: '>=10' } type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + } + engines: { node: '>=8' } type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + } + engines: { node: '>=8' } type-fest@4.26.1: - resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== + } + engines: { node: '>=16' } type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + } + engines: { node: '>= 0.6' } type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + } + engines: { node: '>= 0.6' } typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + } + engines: { node: '>= 0.4' } typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + } + engines: { node: '>= 0.4' } typed-array-byte-offset@1.0.2: - resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + } + engines: { node: '>= 0.4' } typed-array-length@1.0.6: - resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + } + engines: { node: '>= 0.4' } typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + resolution: + { + integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + } typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} + resolution: + { + integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + } + engines: { node: '>=4.2.0' } hasBin: true typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== + } + engines: { node: '>=14.17' } hasBin: true typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + } + engines: { node: '>=14.17' } hasBin: true uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + resolution: + { + integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + } ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + resolution: + { + integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + } uid@2.0.2: - resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== + } + engines: { node: '>=8' } uint8array-extras@1.5.0: - resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} - engines: {node: '>=18'} - - ultron@1.0.2: - resolution: {integrity: sha512-QMpnpVtYaWEeY+MwKDN/UdKlE/LsFZXM5lO1u7GaZzNgmIbGixHEmVMIKT+vqYOALu3m5GYQy9kz4Xu4IVn7Ow==} + resolution: + { + integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A== + } + engines: { node: '>=18' } unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + resolution: + { + integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + } undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + resolution: + { + integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + } undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + resolution: + { + integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + } undici@6.20.1: - resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==} - engines: {node: '>=18.17'} + resolution: + { + integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA== + } + engines: { node: '>=18.17' } undici@6.25.0: - resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} - engines: {node: '>=18.17'} + resolution: + { + integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg== + } + engines: { node: '>=18.17' } unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + } + engines: { node: '>=18' } unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== + } + engines: { node: '>=18' } unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + resolution: + { + integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + } unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + resolution: + { + integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + } unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + resolution: + { + integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + } unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + resolution: + { + integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + } unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + resolution: + { + integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + } universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + } + engines: { node: '>= 4.0.0' } universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + resolution: + { + integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + } + engines: { node: '>= 10.0.0' } unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + } + engines: { node: '>= 0.8' } upath@2.0.1: - resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + } + engines: { node: '>=4' } update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + resolution: + { + integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + } hasBin: true peerDependencies: browserslist: '>= 4.21.0' uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - utf-8-validate@1.1.0: - resolution: {integrity: sha512-Qsgu1u2akdyOneurUEVf/tXhkqBQMoE3x0GY5+P7ayPHvVzTqOkkgBhtl26wm6ap10Amhwy0jIhPwMGywx76QQ==} + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + } util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + } v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + resolution: + { + integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + } v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + resolution: + { + integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + } + engines: { node: '>=10.12.0' } validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + resolution: + { + integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + } validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + } + engines: { node: '>= 0.10' } varint@6.0.0: - resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + resolution: + { + integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== + } vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + } + engines: { node: '>= 0.8' } vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + resolution: + { + integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + } vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + resolution: + { + integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + } vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true vite-node@2.1.4: - resolution: {integrity: sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg== + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true vite-plugin-dts@3.9.1: - resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} - engines: {node: ^14.18.0 || >=16.0.0} + resolution: + { + integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg== + } + engines: { node: ^14.18.0 || >=16.0.0 } peerDependencies: typescript: '*' vite: '*' @@ -7123,8 +12077,11 @@ packages: optional: true vite@5.4.9: - resolution: {integrity: sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg== + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@types/node': ^18.0.0 || >=20.0.0 @@ -7154,8 +12111,11 @@ packages: optional: true vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 @@ -7194,8 +12154,11 @@ packages: optional: true vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA== + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -7219,8 +12182,11 @@ packages: optional: true vitest@2.1.4: - resolution: {integrity: sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ== + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -7244,8 +12210,11 @@ packages: optional: true vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + resolution: + { + integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ== + } + engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -7278,21 +12247,33 @@ packages: optional: true vue-router@4.4.5: - resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==} + resolution: + { + integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q== + } peerDependencies: vue: ^3.2.0 vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + resolution: + { + integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ== + } vue-tsc@1.8.27: - resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + resolution: + { + integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg== + } hasBin: true peerDependencies: typescript: '*' vue@3.5.12: - resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==} + resolution: + { + integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg== + } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -7300,7 +12281,10 @@ packages: optional: true vue@3.5.13: - resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + resolution: + { + integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== + } peerDependencies: typescript: '*' peerDependenciesMeta: @@ -7308,8 +12292,11 @@ packages: optional: true vuepress-plugin-components@2.0.0-rc.62: - resolution: {integrity: sha512-01ZI/xJb20VveFQuTR6mABhWk8onO+LPJvkbtq3sZi2eQkg9E16STf0g40BhX7ak2og9xXmPthGNIZ+raQqhxQ==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + resolution: + { + integrity: sha512-01ZI/xJb20VveFQuTR6mABhWk8onO+LPJvkbtq3sZi2eQkg9E16STf0g40BhX7ak2og9xXmPthGNIZ+raQqhxQ== + } + engines: { node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2' } peerDependencies: artplayer: ^5.0.0 dashjs: 4.7.4 @@ -7339,8 +12326,11 @@ packages: optional: true vuepress-plugin-md-enhance@2.0.0-rc.62: - resolution: {integrity: sha512-g0ndft7wPkn2NTs5e7WU1+Ed8BoVhHycivmiAa0tPsfHfcUX984T07qpR0Wq8jmULHSfubH2ywCY7Jd85Q66IA==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + resolution: + { + integrity: sha512-g0ndft7wPkn2NTs5e7WU1+Ed8BoVhHycivmiAa0tPsfHfcUX984T07qpR0Wq8jmULHSfubH2ywCY7Jd85Q66IA== + } + engines: { node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2' } peerDependencies: '@vue/repl': ^4.1.1 chart.js: ^4.0.0 @@ -7385,14 +12375,20 @@ packages: optional: true vuepress-shared@2.0.0-rc.62: - resolution: {integrity: sha512-moG6312I/6YVwyflMsPKJJe6oW3EVjEQe+wNi7NybaRpMV+SAw/5oJS+M95yCDsIt6ppi7stGJJ/CpGWZ1xnbQ==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + resolution: + { + integrity: sha512-moG6312I/6YVwyflMsPKJJe6oW3EVjEQe+wNi7NybaRpMV+SAw/5oJS+M95yCDsIt6ppi7stGJJ/CpGWZ1xnbQ== + } + engines: { node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2' } peerDependencies: vuepress: 2.0.0-rc.18 vuepress-theme-hope@2.0.0-rc.62: - resolution: {integrity: sha512-D15kVuK/cFvajboWnnhLCcwGg8ioYw3E96vX/Sl2o+ARgdGhz6mEQF17AIRZ+Q45yMH7WL2appw9f5Zfm4i6Fw==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + resolution: + { + integrity: sha512-D15kVuK/cFvajboWnnhLCcwGg8ioYw3E96vX/Sl2o+ARgdGhz6mEQF17AIRZ+Q45yMH7WL2appw9f5Zfm4i6Fw== + } + engines: { node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2' } peerDependencies: '@vuepress/plugin-docsearch': 2.0.0-rc.65 '@vuepress/plugin-feed': 2.0.0-rc.65 @@ -7431,8 +12427,11 @@ packages: optional: true vuepress@2.0.0-rc.18: - resolution: {integrity: sha512-TFpePHTIMiUbiJcHTgD4Wc5eBlsxBnhv36F/eM2vbDoeutcS1dGrNtZoKUxrZDXTeZH+q8vrZ3CiBCsHw3K7eA==} - engines: {node: ^18.19.0 || >=20.4.0} + resolution: + { + integrity: sha512-TFpePHTIMiUbiJcHTgD4Wc5eBlsxBnhv36F/eM2vbDoeutcS1dGrNtZoKUxrZDXTeZH+q8vrZ3CiBCsHw3K7eA== + } + engines: { node: ^18.19.0 || >=20.4.0 } hasBin: true peerDependencies: '@vuepress/bundler-vite': 2.0.0-rc.18 @@ -7445,33 +12444,57 @@ packages: optional: true walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + resolution: + { + integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + } watchpack@2.4.1: - resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== + } + engines: { node: '>=10.13.0' } watermark-js-plus@1.5.7: - resolution: {integrity: sha512-KaQEUnvBX5em2hBeuKcpAASpV+sO1j8NbXY7FL7jb0w1TCKmMSyn8nkj2e+KeleuQ1iwyXHEMFdSWXDIQsACYQ==} - engines: {node: '>=20.0.0'} + resolution: + { + integrity: sha512-KaQEUnvBX5em2hBeuKcpAASpV+sO1j8NbXY7FL7jb0w1TCKmMSyn8nkj2e+KeleuQ1iwyXHEMFdSWXDIQsACYQ== + } + engines: { node: '>=20.0.0' } wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + resolution: + { + integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + } webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + resolution: + { + integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + } webpack-node-externals@3.0.0: - resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + } + engines: { node: '>=6' } webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + } + engines: { node: '>=10.13.0' } webpack@5.82.1: - resolution: {integrity: sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw== + } + engines: { node: '>=10.13.0' } hasBin: true peerDependencies: webpack-cli: '*' @@ -7480,86 +12503,143 @@ packages: optional: true whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + } + engines: { node: '>=18' } deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + } + engines: { node: '>=18' } whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + resolution: + { + integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + } which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + resolution: + { + integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + } which-builtin-type@1.1.3: - resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + } + engines: { node: '>= 0.4' } which-collection@1.0.1: - resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + resolution: + { + integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + } which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + resolution: + { + integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + } which-typed-array@1.1.13: - resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + } + engines: { node: '>= 0.4' } which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + } + engines: { node: '>= 0.4' } which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + } + engines: { node: '>= 8' } hasBin: true why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + } + engines: { node: '>=8' } hasBin: true why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + } + engines: { node: '>=8' } hasBin: true windows-release@4.0.0: - resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== + } + engines: { node: '>=10' } word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + } + engines: { node: '>=0.10.0' } wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + } + engines: { node: '>=8' } wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + } + engines: { node: '>=10' } wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + } + engines: { node: '>=18' } wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + } write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - ws@0.7.2: - resolution: {integrity: sha512-8mJ1Ku743qD/PKmO9Dg+y7BXTwzUgKdXguecfIyOVHFmez4JMqMF+V+M684btmQXHlwzyrJqRl3NYDltGDf6CQ==} + resolution: + { + integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + } + engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.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' @@ -7570,76 +12650,129 @@ packages: optional: true y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + resolution: + { + integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + } y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + } + engines: { node: '>=10' } yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + } yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + } yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + } + engines: { node: '>= 6' } yaml@2.5.1: - resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== + } + engines: { node: '>= 14' } hasBin: true yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + } + engines: { node: '>=6' } yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + } + engines: { node: '>=12' } yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + } + engines: { node: '>=8' } yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + } + engines: { node: '>=12' } ylru@1.4.0: - resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA== + } + engines: { node: '>= 4.0.0' } yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + } + engines: { node: '>=6' } yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + } + engines: { node: '>=10' } yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + } + engines: { node: '>=12.20' } yoctocolors@2.0.2: - resolution: {integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw== + } + engines: { node: '>=18' } z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} + resolution: + { + integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q== + } + engines: { node: '>=8.0.0' } hasBin: true zod@3.23.0: - resolution: {integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==} + resolution: + { + integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA== + } zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + resolution: + { + integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== + } snapshots: - '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -8027,147 +13160,225 @@ snapshots: '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.27.2': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.27.2': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.27.2': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -8958,6 +14169,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + '@rollup/pluginutils@5.1.2(rollup@4.57.0)': dependencies: '@types/estree': 1.0.6 @@ -9773,7 +14988,7 @@ snapshots: vite: 5.4.9(@types/node@22.7.5)(sass-embedded@1.82.0)(terser@5.34.1) vue: 3.5.12(typescript@5.6.3) - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -9785,7 +15000,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1) + vitest: 4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1) '@vitest/expect@1.6.0': dependencies: @@ -9817,13 +15032,13 @@ snapshots: optionalDependencies: vite: 5.4.9(@types/node@20.16.11)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1) - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1) + vite: 7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1) '@vitest/pretty-format@2.1.4': dependencies: @@ -10068,7 +15283,7 @@ snapshots: '@vue/shared@3.5.13': {} - '@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1)': + '@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1)': dependencies: '@vitejs/plugin-vue': 5.1.4(vite@5.4.9(@types/node@22.7.5)(sass-embedded@1.82.0)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3)) '@vuepress/bundlerutils': 2.0.0-rc.18(typescript@5.6.3) @@ -10079,7 +15294,7 @@ snapshots: autoprefixer: 10.4.20(postcss@8.4.47) connect-history-api-fallback: 2.0.0 postcss: 8.4.47 - postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.47)(yaml@2.5.1) + postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.47)(tsx@4.23.12)(yaml@2.5.1) rollup: 4.24.0 vite: 5.4.9(@types/node@22.7.5)(sass-embedded@1.82.0)(terser@5.34.1) vue: 3.5.12(typescript@5.6.3) @@ -10144,7 +15359,7 @@ snapshots: - supports-color - typescript - '@vuepress/helper@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/helper@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@vue/shared': 3.5.13 '@vueuse/core': 12.0.0(typescript@5.6.3) @@ -10152,11 +15367,11 @@ snapshots: fflate: 0.8.2 gray-matter: 4.0.3 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/helper@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/helper@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@vue/shared': 3.5.13 '@vueuse/core': 12.0.0(typescript@5.6.3) @@ -10164,13 +15379,13 @@ snapshots: fflate: 0.8.2 gray-matter: 4.0.3 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/highlighter-helper@2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/highlighter-helper@2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) optionalDependencies: '@vueuse/core': 12.0.0(typescript@5.6.3) @@ -10195,140 +15410,140 @@ snapshots: transitivePeerDependencies: - supports-color - '@vuepress/plugin-active-header-links@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-active-header-links@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-back-to-top@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-back-to-top@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-blog@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-blog@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) chokidar: 3.6.0 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-catalog@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-catalog@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-comment@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-comment@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) giscus: 1.5.0 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-copy-code@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-copy-code@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-copyright@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-copyright@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-git@2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-git@2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: execa: 9.5.2 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) - '@vuepress/plugin-links-check@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-links-check@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-markdown-ext@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-ext@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-container': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-footnote': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-tasklist': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) js-yaml: 4.1.0 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-hint@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-hint@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-alert': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-container': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-image@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-image@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-figure': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-img-lazyload': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-img-mark': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-img-size': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-include@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-include@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-include': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-math@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-math@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-katex-slim': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-mathjax-slim': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-stylize@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-stylize@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-align': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-attrs': 0.14.0(markdown-it@14.1.0) @@ -10338,172 +15553,172 @@ snapshots: '@mdit/plugin-sub': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-sup': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-markdown-tab@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-markdown-tab@2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@mdit/plugin-tab': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - markdown-it - typescript - '@vuepress/plugin-medium-zoom@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-medium-zoom@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) medium-zoom: 1.1.0 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-notice@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-notice@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-nprogress@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-nprogress@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-palette@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-palette@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) chokidar: 4.0.1 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-photo-swipe@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-photo-swipe@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) photoswipe: 5.4.4 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/highlighter-helper': 2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/highlighter-helper': 2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) prismjs: 1.29.0 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - '@vueuse/core' - typescript - '@vuepress/plugin-reading-time@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-reading-time@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-redirect@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-redirect@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) cac: 6.7.14 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-rtl@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-rtl@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-sass-palette@2.0.0-rc.63(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-sass-palette@2.0.0-rc.63(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) chokidar: 4.0.1 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) optionalDependencies: sass-embedded: 1.82.0 transitivePeerDependencies: - typescript - '@vuepress/plugin-sass-palette@2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-sass-palette@2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) chokidar: 4.0.1 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) optionalDependencies: sass-embedded: 1.82.0 transitivePeerDependencies: - typescript - '@vuepress/plugin-seo@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-seo@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-shiki@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-shiki@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@shikijs/transformers': 1.24.1 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/highlighter-helper': 2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/highlighter-helper': 2.0.0-rc.62(@vueuse/core@12.0.0(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) nanoid: 5.0.9 shiki: 1.24.1 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - '@vueuse/core' - typescript - '@vuepress/plugin-sitemap@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-sitemap@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) sitemap: 8.0.0 - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-theme-data@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-theme-data@2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@vue/devtools-api': 7.6.7 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-theme-data@2.0.0-rc.64(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-theme-data@2.0.0-rc.64(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: '@vue/devtools-api': 7.6.7 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - '@vuepress/plugin-watermark@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + '@vuepress/plugin-watermark@2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) watermark-js-plus: 1.5.7 transitivePeerDependencies: - typescript @@ -10512,26 +15727,26 @@ snapshots: dependencies: '@mdit-vue/types': 2.1.0 - '@vuepress/theme-default@2.0.0-rc.65(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': - dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-active-header-links': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-back-to-top': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-copy-code': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-git': 2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-links-check': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-hint': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-tab': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-medium-zoom': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-nprogress': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-palette': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-prismjs': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-seo': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-sitemap': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-theme-data': 2.0.0-rc.64(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/theme-default@2.0.0-rc.65(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-active-header-links': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-back-to-top': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-copy-code': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-git': 2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-links-check': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-hint': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-tab': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-medium-zoom': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-nprogress': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-palette': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-prismjs': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-seo': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-sitemap': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-theme-data': 2.0.0-rc.64(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) optionalDependencies: sass-embedded: 1.82.0 transitivePeerDependencies: @@ -10940,9 +16155,6 @@ snapshots: binary-extensions@2.3.0: {} - bindings@1.2.1: - optional: true - birpc@0.2.19: {} bl@4.1.0: @@ -11004,12 +16216,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bufferutil@1.1.0: - dependencies: - bindings: 1.2.1 - nan: 1.8.4 - optional: true - bufferutil@4.0.9: dependencies: node-gyp-build: 4.8.4 @@ -11623,10 +16829,6 @@ snapshots: dependencies: once: 1.4.0 - endpoint@0.4.5: - dependencies: - inherits: 2.0.4 - enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -11820,6 +17022,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -12415,6 +17646,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -12803,11 +18037,6 @@ snapshots: through: 2.3.8 wrap-ansi: 7.0.0 - inspector@0.5.0: - dependencies: - endpoint: 0.4.5 - ws: 0.7.2 - internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -13891,9 +19120,6 @@ snapshots: mute-stream@0.0.8: {} - nan@1.8.4: - optional: true - nanoid@3.3.11: {} nanoid@3.3.7: {} @@ -14070,8 +19296,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - options@0.0.6: {} - ora@5.4.1: dependencies: bl: 4.1.0 @@ -14228,18 +19452,27 @@ snapshots: mlly: 1.7.2 pathe: 1.1.2 + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} pngjs@5.0.0: {} possible-typed-array-names@1.0.0: {} - postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.47)(yaml@2.5.1): + postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.47)(tsx@4.23.12)(yaml@2.5.1): dependencies: lilconfig: 3.1.2 optionalDependencies: jiti: 1.21.6 postcss: 8.4.47 + tsx: 4.23.12 yaml: 2.5.1 postcss-value-parser@4.2.0: {} @@ -15286,6 +20519,12 @@ snapshots: tslib: 1.14.1 typescript: 5.6.3 + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + turbo-darwin-64@1.13.4: optional: true @@ -15392,8 +20631,6 @@ snapshots: uint8array-extras@1.5.0: {} - ultron@1.0.2: {} - unbox-primitive@1.0.2: dependencies: call-bind: 1.0.7 @@ -15454,12 +20691,6 @@ snapshots: dependencies: punycode: 2.3.1 - utf-8-validate@1.1.0: - dependencies: - bindings: 1.2.1 - nan: 1.8.4 - optional: true - util-deprecate@1.0.2: {} v8-compile-cache-lib@3.0.1: {} @@ -15566,7 +20797,7 @@ snapshots: sass-embedded: 1.82.0 terser: 5.34.1 - vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1): + vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -15581,6 +20812,7 @@ snapshots: sass: 1.79.5 sass-embedded: 1.82.0 terser: 5.34.1 + tsx: 4.23.12 yaml: 2.5.1 vitest@1.6.0(@types/node@20.16.11)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1): @@ -15652,10 +20884,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1): + vitest@4.0.18(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -15672,7 +20904,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(yaml@2.5.1) + vite: 7.3.1(@types/node@20.16.11)(jiti@1.21.6)(sass-embedded@1.82.0)(sass@1.79.5)(terser@5.34.1)(tsx@4.23.12)(yaml@2.5.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.16.11 @@ -15726,96 +20958,96 @@ snapshots: optionalDependencies: typescript: 5.6.3 - vuepress-plugin-components@2.0.0-rc.62(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): + vuepress-plugin-components@2.0.0-rc.62(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): dependencies: '@stackblitz/sdk': 1.11.0 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-sass-palette': 2.0.0-rc.63(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.63(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) balloon-css: 1.2.0 create-codepen: 2.0.0 qrcode: 1.5.4 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) - vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) optionalDependencies: sass-embedded: 1.82.0 transitivePeerDependencies: - typescript - vuepress-plugin-md-enhance@2.0.0-rc.62(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): + vuepress-plugin-md-enhance@2.0.0-rc.62(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): dependencies: '@mdit/plugin-container': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-demo': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-plantuml': 0.14.0(markdown-it@14.1.0) '@mdit/plugin-uml': 0.14.0(markdown-it@14.1.0) '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-sass-palette': 2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) balloon-css: 1.2.0 js-yaml: 4.1.0 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) - vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) optionalDependencies: sass-embedded: 1.82.0 transitivePeerDependencies: - markdown-it - typescript - vuepress-shared@2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): + vuepress-shared@2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) dayjs: 1.11.13 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) transitivePeerDependencies: - typescript - vuepress-theme-hope@2.0.0-rc.62(@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))))(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): - dependencies: - '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-active-header-links': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-back-to-top': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-blog': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-catalog': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-comment': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-copy-code': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-copyright': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-git': 2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-links-check': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-ext': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-hint': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-image': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-include': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-math': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-stylize': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-markdown-tab': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-notice': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-nprogress': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-photo-swipe': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-reading-time': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-redirect': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-rtl': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-sass-palette': 2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-seo': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-shiki': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-sitemap': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-theme-data': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - '@vuepress/plugin-watermark': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress-theme-hope@2.0.0-rc.62(@vuepress/plugin-prismjs@2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))))(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))): + dependencies: + '@vuepress/helper': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-active-header-links': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-back-to-top': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-blog': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-catalog': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-comment': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-copy-code': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-copyright': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-git': 2.0.0-rc.62(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-links-check': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-ext': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-hint': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-image': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-include': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-math': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-stylize': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-markdown-tab': 2.0.0-rc.65(markdown-it@14.1.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-notice': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-nprogress': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-photo-swipe': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-reading-time': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-redirect': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-rtl': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.65(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-seo': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-shiki': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-sitemap': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-theme-data': 2.0.0-rc.63(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-watermark': 2.0.0-rc.65(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) '@vueuse/core': 12.0.0(typescript@5.6.3) balloon-css: 1.2.0 bcrypt-ts: 5.0.3 chokidar: 3.6.0 vue: 3.5.13(typescript@5.6.3) - vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) - vuepress-plugin-components: 2.0.0-rc.62(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress-plugin-md-enhance: 2.0.0-rc.62(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) - vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + vuepress-plugin-components: 2.0.0-rc.62(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress-plugin-md-enhance: 2.0.0-rc.62(markdown-it@14.1.0)(sass-embedded@1.82.0)(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + vuepress-shared: 2.0.0-rc.62(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) optionalDependencies: - '@vuepress/plugin-prismjs': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) + '@vuepress/plugin-prismjs': 2.0.0-rc.65(@vueuse/core@12.0.0(typescript@5.6.3))(typescript@5.6.3)(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))) sass-embedded: 1.82.0 transitivePeerDependencies: - '@vue/repl' @@ -15841,7 +21073,7 @@ snapshots: - typescript - vidstack - vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)): + vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)): dependencies: '@vuepress/cli': 2.0.0-rc.18(typescript@5.6.3) '@vuepress/client': 2.0.0-rc.18(typescript@5.6.3) @@ -15851,7 +21083,7 @@ snapshots: '@vuepress/utils': 2.0.0-rc.18 vue: 3.5.12(typescript@5.6.3) optionalDependencies: - '@vuepress/bundler-vite': 2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(typescript@5.6.3)(yaml@2.5.1) + '@vuepress/bundler-vite': 2.0.0-rc.18(@types/node@22.7.5)(jiti@1.21.6)(sass-embedded@1.82.0)(terser@5.34.1)(tsx@4.23.12)(typescript@5.6.3)(yaml@2.5.1) transitivePeerDependencies: - supports-color - typescript @@ -16012,14 +21244,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@0.7.2: - dependencies: - options: 0.0.6 - ultron: 1.0.2 - optionalDependencies: - bufferutil: 1.1.0 - utf-8-validate: 1.1.0 - ws@8.20.0(bufferutil@4.0.9): optionalDependencies: bufferutil: 4.0.9