├── pnpm-workspace.yaml
├── .vscode
├── extensions.json
└── settings.json
├── .env.template
├── .gitignore
├── devtools
├── src
│ ├── main.tsx
│ ├── vite-env.d.ts
│ ├── components
│ │ ├── DatasetConnectionState.tsx
│ │ ├── DebugToggle.tsx
│ │ ├── AudioToggle.tsx
│ │ ├── VideoToggle.tsx
│ │ ├── ClientId.tsx
│ │ ├── StandaloneToggle.tsx
│ │ ├── AyameWebSdkVersion.tsx
│ │ ├── SignalingUrl.tsx
│ │ ├── CameraPermissionState.tsx
│ │ ├── RoomId.tsx
│ │ ├── CopyUrlButton.tsx
│ │ ├── SignalingKey.tsx
│ │ ├── MicrophonePermissionState.tsx
│ │ ├── VideoResolution.tsx
│ │ ├── RemoteVideo.tsx
│ │ ├── LocalVideo.tsx
│ │ ├── TransceiverDirection.tsx
│ │ ├── ConnectionSettings.tsx
│ │ ├── DisconnectButton.tsx
│ │ ├── AudioCodecMimeType.tsx
│ │ ├── VideoCodecMimeType.tsx
│ │ ├── VideoInputDevice.tsx
│ │ ├── AudioInputDevice.tsx
│ │ ├── AudioOutputDevice.tsx
│ │ ├── MediaSettings.tsx
│ │ ├── RequestMediaPermissionButton.tsx
│ │ └── ConnectButton.tsx
│ ├── store
│ │ ├── useStore.ts
│ │ ├── createDeviceSlice.ts
│ │ ├── createPermissionSlice.ts
│ │ ├── createAyameSlice.ts
│ │ ├── signals.ts
│ │ └── createSettingsSlice.ts
│ └── App.tsx
├── index.html
├── package.json
├── vite.config.mjs
└── tsconfig.json
├── typedoc.json
├── tests
├── version.test.ts
├── devtools.test.ts
└── codec.test.ts
├── CLAUDE.md
├── .github
├── renovate.json
└── workflows
│ ├── ci.yml
│ ├── e2e-test.yml
│ └── deploy.yml
├── tsconfig.json
├── src
├── types.d.ts
├── utils.ts
└── ayame.ts
├── vite.config.mjs
├── playwright.config.mts
├── biome.jsonc
├── package.json
├── README.md
├── CHANGES.md
├── canary.py
├── LICENSE
└── pnpm-lock.yaml
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "devtools"
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["biomejs.biome"]
3 | }
4 |
--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------
1 | VITE_AYAME_SIGNALING_URL=wss://ayame-labo.shiguredo.app/signaling
2 | VITE_AYAME_ROOM_ID_PREFIX=github-login-name@
3 | VITE_AYAME_ROOM_NAME=ayame-devtools
4 | VITE_AYAME_SIGNALING_KEY=signaling-key
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | typedoc/
3 | apidoc/
4 | .log/
5 | dist/
6 | .DS_Store
7 |
8 | # .env
9 | .env*
10 | !.env.template
11 |
12 | # playwright
13 | /test-results/
14 | /playwright-report/
15 | /blob-report/
16 | /playwright/.cache/
17 |
--------------------------------------------------------------------------------
/devtools/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App'
4 |
5 | const root = createRoot(document.getElementById('root') as HTMLDivElement)
6 | root.render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": [
3 | "./src/ayame.ts",
4 | "./src/types.d.ts"
5 | ],
6 | "tsconfig": "./tsconfig.json",
7 | "disableSources": true,
8 | "excludePrivate": true,
9 | "excludeProtected": true,
10 | "readme": "./README.md",
11 | "out": "typedoc"
12 | }
--------------------------------------------------------------------------------
/devtools/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | VITE_AYAME_SIGNALING_URL: string
5 | VITE_AYAME_ROOM_ID_PREFIX: string
6 | VITE_AYAME_ROOM_NAME: string
7 | VITE_AYAME_SIGNALING_KEY: string
8 | }
9 |
10 | interface ImportMeta {
11 | readonly env: ImportMetaEnv
12 | }
13 |
--------------------------------------------------------------------------------
/devtools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ayame DevTools
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/devtools/src/components/DatasetConnectionState.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../store/useStore'
2 |
3 | const DatasetConnectionState: React.FC = () => {
4 | const ayameConnectionState = useStore((state) => state.ayame.connectionState)
5 |
6 | // playwright の E2E テスト用
7 | return
8 | }
9 |
10 | export default DatasetConnectionState
11 |
--------------------------------------------------------------------------------
/devtools/src/components/DebugToggle.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const DebugToggle: React.FC = () => {
5 | const isEnable = useStore((state) => state.settings.debug)
6 | const toggleDebug = useStore((state) => state.toggleDebug)
7 |
8 | return (
9 | toggleDebug(e.target.checked)} />
10 | )
11 | }
12 |
13 | export default DebugToggle
14 |
--------------------------------------------------------------------------------
/devtools/src/components/AudioToggle.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const AudioToggle: React.FC = () => {
5 | const isEnable = useStore((state) => state.settings.audio.isEnable)
6 | const toggleAudio = useStore((state) => state.toggleAudio)
7 |
8 | return (
9 | toggleAudio(e.target.checked)} />
10 | )
11 | }
12 |
13 | export default AudioToggle
14 |
--------------------------------------------------------------------------------
/devtools/src/components/VideoToggle.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const VideoToggle: React.FC = () => {
5 | const isEnable = useStore((state) => state.settings.video.isEnable)
6 | const toggleVideo = useStore((state) => state.toggleVideo)
7 |
8 | return (
9 | toggleVideo(e.target.checked)} />
10 | )
11 | }
12 |
13 | export default VideoToggle
14 |
--------------------------------------------------------------------------------
/tests/version.test.ts:
--------------------------------------------------------------------------------
1 | import { version } from '@open-ayame/ayame-web-sdk'
2 | import { expect, test } from '@playwright/test'
3 |
4 | test('Ayame Web SDK のバージョンを確認', async ({ browser }) => {
5 | const sendrecv1 = await browser.newPage()
6 |
7 | await sendrecv1.goto('http://localhost:9000/')
8 |
9 | // Ayame Web SDK のバージョンを確認
10 | await expect(sendrecv1.locator('[data-testid="ayame-web-sdk-version"]')).toHaveText(version(), {
11 | timeout: 10000,
12 | })
13 |
14 | await sendrecv1.close()
15 | })
16 |
--------------------------------------------------------------------------------
/devtools/src/components/ClientId.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../store/useStore'
2 |
3 | const ClientId = () => {
4 | const clientId = useStore((state) => state.settings.clientId)
5 | const setClientId = useStore((state) => state.setClientId)
6 |
7 | const handleChange = (e: React.ChangeEvent) => {
8 | setClientId(e.target.value)
9 | }
10 |
11 | return
12 | }
13 |
14 | export default ClientId
15 |
--------------------------------------------------------------------------------
/devtools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-ayame/ayame-devtools",
3 | "scripts": {
4 | "lint": "biome lint .",
5 | "fmt": "biome format --write .",
6 | "check": "tsc --noEmit"
7 | },
8 | "dependencies": {
9 | "@open-ayame/ayame-web-sdk": "workspace:*",
10 | "react": "19.2.3",
11 | "react-dom": "19.2.3",
12 | "zustand": "5.0.9"
13 | },
14 | "devDependencies": {
15 | "@types/react": "19.2.7",
16 | "@types/react-dom": "19.2.3",
17 | "@vitejs/plugin-react": "5.1.2"
18 | }
19 | }
--------------------------------------------------------------------------------
/devtools/src/components/StandaloneToggle.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const StandaloneToggle: React.FC = () => {
5 | const isEnable = useStore((state) => state.settings.standalone)
6 | const toggleStandalone = useStore((state) => state.toggleStandalone)
7 |
8 | return (
9 | toggleStandalone(e.target.checked)}
13 | />
14 | )
15 | }
16 |
17 | export default StandaloneToggle
18 |
--------------------------------------------------------------------------------
/devtools/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { resolve } from 'node:path'
2 | import react from '@vitejs/plugin-react'
3 | import { defineConfig } from 'vite'
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: process.env.NODE_ENV === 'production' ? '/ayame-web-sdk/devtools/' : '/',
8 | root: resolve(__dirname),
9 | dist: resolve(__dirname, 'dist'),
10 | resolve: {
11 | alias: {
12 | '@open-ayame/ayame-web-sdk': resolve(__dirname, '../dist/ayame.mjs'),
13 | },
14 | },
15 | envDir: resolve(__dirname, '..'),
16 | })
17 |
--------------------------------------------------------------------------------
/devtools/src/components/AyameWebSdkVersion.tsx:
--------------------------------------------------------------------------------
1 | import { version } from '@open-ayame/ayame-web-sdk'
2 | import { useEffect } from 'react'
3 | import { useStore } from '../store/useStore'
4 | const AyameVersion = () => {
5 | const ayameVersion = useStore((state) => state.ayame.version)
6 | const setAyameVersion = useStore((state) => state.setAyameVersion)
7 |
8 | useEffect(() => {
9 | setAyameVersion(version())
10 | }, [setAyameVersion])
11 |
12 | return {version()}
13 | }
14 |
15 | export default AyameVersion
16 |
--------------------------------------------------------------------------------
/devtools/src/components/SignalingUrl.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../store/useStore'
2 |
3 | const SignalingUrl = () => {
4 | const signalingUrl = useStore((state) => state.settings.signalingUrl)
5 | const setSignalingUrl = useStore((state) => state.setSignalingUrl)
6 |
7 | const handleChange = (e: React.ChangeEvent) => {
8 | setSignalingUrl(e.target.value)
9 | }
10 |
11 | return (
12 |
13 | )
14 | }
15 |
16 | export default SignalingUrl
17 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | - Premature Optimization is the Root of All Evil
4 | - 一切忖度しないこと
5 | - 常に日本語を利用すること
6 | - 全角と半角の間には半角スペースを入れること
7 |
8 | ## レビューについて
9 |
10 | - レビューはかなり厳しくすること
11 | - レビューの表現は、シンプルにすること
12 | - レビューの表現は、日本語で行うこと
13 | - レビューの表現は、指摘内容を明確にすること
14 | - レビューの表現は、指摘内容を具体的にすること
15 | - レビューの表現は、指摘内容を優先順位をつけること
16 | - レビューの表現は、指摘内容を優先順位をつけて、重要なものから順に記載すること
17 | - ドキュメントは別に書いているので、ドキュメトに付いては考慮しないこと
18 | - 変更点とリリースノートの整合性を確認すること
19 |
20 | ## コミットについて
21 |
22 | - 勝手にコミットしないこと
23 | - コミットメッセージは確認すること
24 | - コミットメッセージは日本語で書くこと
25 | - コミットメッセージは命令形で書くこと
26 | - コミットメッセージは〜するという形で書くこと
27 |
--------------------------------------------------------------------------------
/devtools/src/components/CameraPermissionState.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const CameraPermissionState: React.FC = () => {
6 | const cameraPermissionState = useStore((state) => state.permissionState.cameraState)
7 | const setCameraPermissionState = useStore((state) => state.setCameraPermissionState)
8 |
9 | useEffect(() => {
10 | setCameraPermissionState()
11 | }, [setCameraPermissionState])
12 |
13 | return <>{cameraPermissionState}>
14 | }
15 |
16 | export default CameraPermissionState
17 |
--------------------------------------------------------------------------------
/devtools/src/components/RoomId.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../store/useStore'
2 |
3 | const RoomId = () => {
4 | const roomId = useStore((state) => state.settings.roomId)
5 | const setRoomId = useStore((state) => state.setRoomId)
6 |
7 | const handleChange = (e: React.ChangeEvent) => {
8 | setRoomId(e.target.value)
9 | }
10 |
11 | return (
12 |
19 | )
20 | }
21 |
22 | export default RoomId
23 |
--------------------------------------------------------------------------------
/devtools/src/components/CopyUrlButton.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const CopyUrlButton: React.FC = () => {
5 | const generateUrlParams = useStore((state) => state.generateUrlParams)
6 |
7 | const handleClick = () => {
8 | const urlParams = generateUrlParams()
9 | window.history.replaceState(null, '', `?${urlParams}`)
10 | navigator.clipboard.writeText(window.location.href)
11 | }
12 |
13 | return (
14 |
17 | )
18 | }
19 |
20 | export default CopyUrlButton
21 |
--------------------------------------------------------------------------------
/devtools/src/components/SignalingKey.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../store/useStore'
2 |
3 | const SignalingKey = () => {
4 | const signalingKey = useStore((state) => state.settings.signalingKey)
5 | const setSignalingKey = useStore((state) => state.setSignalingKey)
6 |
7 | const handleChange = (e: React.ChangeEvent) => {
8 | setSignalingKey(e.target.value)
9 | }
10 |
11 | return (
12 |
18 | )
19 | }
20 |
21 | export default SignalingKey
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "[html]": {
4 | "editor.defaultFormatter": "vscode.html-language-features"
5 | },
6 | "[javascript]": {
7 | "editor.defaultFormatter": "biomejs.biome"
8 | },
9 | "[javascriptreact]": {
10 | "editor.defaultFormatter": "biomejs.biome"
11 | },
12 | "[typescript]": {
13 | "editor.defaultFormatter": "biomejs.biome"
14 | },
15 | "[typescriptreact]": {
16 | "editor.defaultFormatter": "biomejs.biome"
17 | },
18 | "editor.codeActionsOnSave": {
19 | "quickfix.biome": "explicit",
20 | "source.organizeImports.biome": "explicit"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/devtools/src/components/MicrophonePermissionState.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const MicrophonePermissionState: React.FC = () => {
6 | const microphonePermissionState = useStore((state) => state.permissionState.microphoneState)
7 | const setMicrophonePermissionState = useStore((state) => state.setMicrophonePermissionState)
8 |
9 | useEffect(() => {
10 | setMicrophonePermissionState()
11 | }, [setMicrophonePermissionState])
12 |
13 | return <>{microphonePermissionState}>
14 | }
15 |
16 | export default MicrophonePermissionState
17 |
--------------------------------------------------------------------------------
/devtools/src/components/VideoResolution.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const VideoResolution: React.FC = () => {
5 | const videoResolution = useStore((state) => state.settings.video.resolution)
6 | const setVideoResolution = useStore((state) => state.setVideoResolution)
7 |
8 | const handleChange = (event: React.ChangeEvent) => {
9 | setVideoResolution(event.target.value)
10 | }
11 |
12 | return (
13 |
19 | )
20 | }
21 |
22 | export default VideoResolution
--------------------------------------------------------------------------------
/devtools/src/store/useStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { type AyameSlice, createAyameSlice } from './createAyameSlice'
3 | import { type MediaDeviceSlice, createMediaDeviceSlice } from './createDeviceSlice'
4 | import { type PermissionSlice, createPermissionSlice } from './createPermissionSlice'
5 | import { type SettingsSlice, createSettingsSlice } from './createSettingsSlice'
6 |
7 | export const useStore = create()(
8 | (...a) => ({
9 | ...createAyameSlice(...a),
10 | ...createPermissionSlice(...a),
11 | ...createMediaDeviceSlice(...a),
12 | ...createSettingsSlice(...a),
13 | }),
14 | )
15 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ],
6 | "dependencyDashboard": false,
7 | "packageRules": [
8 | {
9 | "matchUpdateTypes": [
10 | "minor",
11 | "patch",
12 | "pin",
13 | "digest"
14 | ],
15 | "platformAutomerge": true,
16 | "automerge": true
17 | },
18 | {
19 | "matchUpdateTypes": [
20 | "minor",
21 | "patch",
22 | "pin",
23 | "digest"
24 | ],
25 | "matchPackagePatterns": [
26 | "rollup"
27 | ],
28 | "groupName": "rollup",
29 | "platformAutomerge": true,
30 | "automerge": true
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | - "feature/*"
8 | - "releases/*"
9 | jobs:
10 | ci_job:
11 | runs-on: ubuntu-24.04
12 | strategy:
13 | matrix:
14 | node-version: ["20", "22", "23"]
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - uses: pnpm/action-setup@v4
21 | with:
22 | version: 10
23 | - run: pnpm install
24 | - run: pnpm run lint
25 | env:
26 | CI: true
27 | - run: pnpm run check
28 | env:
29 | CI: true
30 | - run: pnpm run build
31 | env:
32 | CI: true
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "strict": true,
6 | "declaration": true,
7 | "strictNullChecks": true,
8 | "importHelpers": true,
9 | "moduleResolution": "Bundler",
10 | "experimentalDecorators": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "resolveJsonModule": true,
14 | "baseUrl": ".",
15 | "newLine": "LF",
16 | "types": ["webrtc", "node"],
17 | "paths": {
18 | "@/*": ["src/*"]
19 | },
20 | "lib": ["dom", "esnext", "scripthost"],
21 | "declarationDir": "./dist",
22 | "outDir": "./dist",
23 | "rootDir": "src"
24 | },
25 | "include": ["src/**/*.ts", "src/**/*.d.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/devtools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "strict": true,
6 | "declaration": true,
7 | "strictNullChecks": true,
8 | "importHelpers": true,
9 | "moduleResolution": "Bundler",
10 | "experimentalDecorators": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "resolveJsonModule": true,
14 | "allowImportingTsExtensions": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 | "newLine": "LF",
18 | "paths": {
19 | "@open-ayame/ayame-web-sdk": ["../dist/ayame.d.ts"]
20 | },
21 | "types": ["webrtc", "node"],
22 | "lib": ["dom", "esnext", "dom.iterable"],
23 | "declarationDir": "."
24 | },
25 | "include": ["src/**/*"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/devtools/src/components/RemoteVideo.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useRef } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const RemoteVideo: React.FC = () => {
6 | const remoteMediaStream = useStore((state) => state.mediaStream.remote)
7 | const videoRef = useRef(null)
8 |
9 | useEffect(() => {
10 | if (videoRef.current) {
11 | videoRef.current.srcObject = remoteMediaStream
12 | }
13 | }, [remoteMediaStream])
14 |
15 | return (
16 |
24 | )
25 | }
26 |
27 | export default RemoteVideo
28 |
--------------------------------------------------------------------------------
/devtools/src/components/LocalVideo.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useRef } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const LocalVideo: React.FC = () => {
6 | const localMediaStream = useStore((state) => state.mediaStream.local)
7 | const videoRef = useRef(null)
8 |
9 | useEffect(() => {
10 | if (videoRef.current) {
11 | videoRef.current.srcObject = localMediaStream
12 | }
13 | }, [localMediaStream])
14 |
15 | return (
16 |
29 | )
30 | }
31 |
32 | export default LocalVideo
33 |
--------------------------------------------------------------------------------
/devtools/src/components/TransceiverDirection.tsx:
--------------------------------------------------------------------------------
1 | import type { Direction as AyameDirection } from '@open-ayame/ayame-web-sdk'
2 | import type React from 'react'
3 |
4 | // 型チェック
5 | type AssertDirection = T
6 | type CheckDirection = AssertDirection
7 |
8 | const DIRECTION = {
9 | SENDRECV: 'sendrecv',
10 | SENDONLY: 'sendonly',
11 | RECVONLY: 'recvonly',
12 | } as const
13 |
14 | type Direction = (typeof DIRECTION)[keyof typeof DIRECTION]
15 |
16 | type Props = {
17 | value: Direction
18 | onChange: (direction: Direction) => void
19 | }
20 |
21 | const TransceiverDirection: React.FC = ({ value, onChange }) => {
22 | return (
23 |
30 | )
31 | }
32 |
33 | export default TransceiverDirection
34 |
--------------------------------------------------------------------------------
/devtools/src/components/ConnectionSettings.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import ClientId from './ClientId'
3 | import DebugToggle from './DebugToggle'
4 | import RoomId from './RoomId'
5 | import SignalingKey from './SignalingKey'
6 | import SignalingUrl from './SignalingUrl'
7 | import StandaloneToggle from './StandaloneToggle'
8 |
9 | const ConnectionSettings: React.FC = () => {
10 | return (
11 |
38 | )
39 | }
40 |
41 | export default ConnectionSettings
42 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Direction = 'sendrecv' | 'recvonly' | 'sendonly'
2 |
3 | /** 音声の設定 */
4 | export interface ConnectionAudioOption {
5 | /** コーデックの MIME type */
6 | codecMimeType?: string
7 | /** 送受信方向 */
8 | direction: Direction
9 | /** 有効かどうかのフラグ */
10 | enabled: boolean
11 | }
12 |
13 | /** 映像の設定 */
14 | export interface ConnectionVideoOption {
15 | /** コーデックの MIME type */
16 | codecMimeType?: string
17 | /** 送受信方向 */
18 | direction: Direction
19 | /** 有効かどうかのフラグ */
20 | enabled: boolean
21 | }
22 |
23 | /** 接続時に指定するオプション */
24 | export interface ConnectionOptions {
25 | /** オーディオの設定 */
26 | audio: ConnectionAudioOption
27 | /** ビデオの設定 */
28 | video: ConnectionVideoOption
29 | /** クライアントID */
30 | clientId: string
31 | /** ayame server から iceServers が返って来なかった場合に使われる iceServer の情報 */
32 | iceServers: RTCIceServer[]
33 | /** 送信するシグナリングキー */
34 | signalingKey?: string
35 | /** standalone モードの場合は true */
36 | standalone?: boolean
37 | }
38 |
39 | /** 接続時に指定できるメタデータ */
40 | export interface MetadataOption {
41 | /** 送信するメタデータ */
42 | authnMetadata?: any
43 | }
44 |
--------------------------------------------------------------------------------
/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { resolve } from 'node:path'
2 | import { defineConfig } from 'vite'
3 | import dts from 'vite-plugin-dts'
4 | import pkg from './package.json'
5 |
6 | const banner = `/**
7 | * ${pkg.name}
8 | * ${pkg.description}
9 | * @version: ${pkg.version}
10 | * @author: ${pkg.author}
11 | * @license: ${pkg.license}
12 | **/
13 | `
14 | export default defineConfig({
15 | define: {
16 | __AYAME_WEB_SDK_VERSION__: JSON.stringify(pkg.version),
17 | },
18 | root: resolve(__dirname, './'),
19 | build: {
20 | minify: 'esbuild',
21 | target: 'es2022',
22 | emptyOutDir: true,
23 | manifest: true,
24 | outDir: resolve(__dirname, './dist'),
25 | lib: {
26 | entry: resolve(__dirname, 'src/ayame.ts'),
27 | name: 'Ayame',
28 | formats: ['es'],
29 | fileName: 'ayame',
30 | },
31 | rollupOptions: {
32 | output: {
33 | // 本来不要なはず
34 | entryFileNames: 'ayame.mjs',
35 | banner: banner,
36 | },
37 | },
38 | },
39 | envDir: resolve(__dirname, './'),
40 | plugins: [
41 | dts({
42 | include: ['src/**/*'],
43 | copyDtsFiles: true,
44 | }),
45 | ],
46 | })
47 |
--------------------------------------------------------------------------------
/playwright.config.mts:
--------------------------------------------------------------------------------
1 | import process from 'node:process'
2 | import { defineConfig, devices } from '@playwright/test'
3 |
4 | export default defineConfig({
5 | workers: 1,
6 | testDir: 'tests',
7 | // fullyParallel: true,
8 | reporter: 'html',
9 | use: {
10 | launchOptions: {
11 | args: [
12 | // CORS 無効
13 | '--disable-web-security',
14 | '--disable-features=IsolateOrigins,site-per-process',
15 |
16 | '--use-fake-ui-for-media-stream',
17 | '--use-fake-device-for-media-stream',
18 | // "--use-file-for-fake-video-capture=/app/sample.mjpeg",
19 | ],
20 | },
21 | },
22 | projects: [
23 | {
24 | name: 'chromium',
25 | use: { ...devices['Desktop Chrome'] },
26 | },
27 |
28 | // {
29 | // name: 'firefox',
30 | // use: { ...devices['Desktop Firefox'] },
31 | // },
32 |
33 | // {
34 | // name: 'webkit',
35 | // use: { ...devices['Desktop Safari'] },
36 | // },
37 | ],
38 | webServer: {
39 | command: 'pnpm run dev --port 9000',
40 | url: 'http://localhost:9000/',
41 | reuseExistingServer: !process.env.CI,
42 | stdout: 'pipe',
43 | stderr: 'pipe',
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/devtools/src/components/DisconnectButton.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 |
4 | const DisconnectButton: React.FC = () => {
5 | const ayameConnection = useStore((state) => state.ayame.connection)
6 |
7 | const localMediaStream = useStore((state) => state.mediaStream.local)
8 |
9 | const setAyameConnection = useStore((state) => state.setAyameConnection)
10 | const setLocalMediaStream = useStore((state) => state.setLocalMediaStream)
11 | const setRemoteMediaStream = useStore((state) => state.setRemoteMediaStream)
12 |
13 | const handleClick = async () => {
14 | if (!ayameConnection) {
15 | return
16 | }
17 |
18 | if (localMediaStream) {
19 | for (const track of localMediaStream.getTracks()) {
20 | track.stop()
21 | }
22 | }
23 |
24 | await ayameConnection.disconnect()
25 |
26 | setLocalMediaStream(null)
27 | setRemoteMediaStream(null)
28 |
29 | // ayameConnection を null にする
30 | setAyameConnection(null)
31 | }
32 |
33 | return (
34 |
37 | )
38 | }
39 |
40 | export default DisconnectButton
41 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-test.yml:
--------------------------------------------------------------------------------
1 | name: e2e-test
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - "**.md"
7 | - "LICENSE"
8 | # branches-ignore:
9 | # - feature/react-zustand
10 | schedule:
11 | # UTC 時間で毎日 2:00 (JST で 11:00) に実行、月曜日から金曜日
12 | - cron: "0 2 * * 1-5"
13 |
14 | jobs:
15 | e2e_test_job:
16 | strategy:
17 | matrix:
18 | node-version: ["20", "22", "23"]
19 | # browser: ["chromium", "firefox", "webkit"]
20 | browser: ["chromium"]
21 | env:
22 | VITE_AYAME_SIGNALING_URL: ${{ secrets.TEST_SIGNALING_URL }}
23 | VITE_AYAME_ROOM_ID_PREFIX: ${{ secrets.TEST_ROOM_ID_PREFIX }}
24 | VITE_AYAME_SIGNALING_KEY: ${{ secrets.TEST_SIGNALING_KEY }}
25 | runs-on: ubuntu-24.04
26 | timeout-minutes: 10
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: actions/setup-node@v4
30 | with:
31 | node-version: ${{ matrix.node-version }}
32 | - uses: pnpm/action-setup@v4
33 | with:
34 | version: 10
35 | - run: pnpm --version
36 | - run: pnpm install
37 | - run: pnpm build
38 | - run: pnpm exec playwright install ${{ matrix.browser }} --with-deps
39 | - run: pnpm exec playwright test --project=${{ matrix.browser }}
40 | env:
41 | VITE_AYAME_ROOM_NAME: ${{ matrix.node-version }}
--------------------------------------------------------------------------------
/devtools/src/store/createDeviceSlice.ts:
--------------------------------------------------------------------------------
1 | import type { StateCreator } from 'zustand'
2 |
3 | export interface MediaDeviceSlice {
4 | mediaDevice: {
5 | audioInputDeviceId: string
6 | audioOutputDeviceId: string
7 |
8 | videoInputDeviceId: string
9 | }
10 |
11 | setAudioInputDeviceId: (deviceId: string) => void
12 | setAudioOutputDeviceId: (deviceId: string) => void
13 |
14 | setVideoInputDeviceId: (deviceId: string) => void
15 | }
16 |
17 | export const createMediaDeviceSlice: StateCreator = (set, get) => ({
18 | // 初期値
19 | mediaDevice: {
20 | audioInputDeviceId: 'default',
21 | audioOutputDeviceId: 'default',
22 | videoInputDeviceId: 'default',
23 | },
24 |
25 | setAudioInputDeviceId: (deviceId: string) =>
26 | set((state) => ({
27 | mediaDevice: {
28 | ...state.mediaDevice,
29 | audioInputDeviceId: deviceId,
30 | },
31 | })),
32 |
33 | setAudioOutputDeviceId: (deviceId: string) =>
34 | set((state) => ({
35 | mediaDevice: {
36 | ...state.mediaDevice,
37 | audioOutputDeviceId: deviceId,
38 | },
39 | })),
40 |
41 | setVideoInputDeviceId: (deviceId: string) => {
42 | set((state) => ({
43 | mediaDevice: {
44 | ...state.mediaDevice,
45 | videoInputDeviceId: deviceId,
46 | },
47 | }))
48 | },
49 | })
50 |
--------------------------------------------------------------------------------
/devtools/src/components/AudioCodecMimeType.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | import { getAvailableCodecs } from '@open-ayame/ayame-web-sdk'
6 |
7 | const VideoCodecMimeType: React.FC = () => {
8 | const [codecs, setCodecs] = useState([])
9 | const setAudioCodecMimeType = useStore((state) => state.setAudioCodecMimeType)
10 | const audioCodecMimeType = useStore((state) => state.settings.audio.codecMimeType)
11 | const audioDirection = useStore((state) => state.settings.audio.direction)
12 |
13 | useEffect(() => {
14 | const mimeTypes = getAvailableCodecs(
15 | 'audio',
16 | audioDirection === 'sendrecv' || audioDirection === 'sendonly' ? 'sender' : 'receiver',
17 | )
18 | setCodecs(mimeTypes)
19 | }, [audioDirection])
20 |
21 | const handleChange = (e: React.ChangeEvent) => {
22 | setAudioCodecMimeType(e.target.value)
23 | }
24 |
25 | return (
26 |
34 | )
35 | }
36 |
37 | export default VideoCodecMimeType
38 |
--------------------------------------------------------------------------------
/devtools/src/components/VideoCodecMimeType.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | import { getAvailableCodecs } from '@open-ayame/ayame-web-sdk'
6 |
7 | const VideoCodecMimeType: React.FC = () => {
8 | const [codecs, setCodecs] = useState([])
9 | const setVideoCodecMimeType = useStore((state) => state.setVideoCodecMimeType)
10 | const videoCodecMimeType = useStore((state) => state.settings.video.codecMimeType)
11 | const videoDirection = useStore((state) => state.settings.video.direction)
12 |
13 | useEffect(() => {
14 | const mimeTypes = getAvailableCodecs(
15 | 'video',
16 | videoDirection === 'sendrecv' || videoDirection === 'sendonly' ? 'sender' : 'receiver',
17 | )
18 | setCodecs(mimeTypes)
19 | }, [videoDirection])
20 |
21 | const handleChange = (e: React.ChangeEvent) => {
22 | setVideoCodecMimeType(e.target.value)
23 | }
24 |
25 | return (
26 |
34 | )
35 | }
36 |
37 | export default VideoCodecMimeType
38 |
--------------------------------------------------------------------------------
/devtools/src/App.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect } from 'react'
3 | import AyameVersion from './components/AyameWebSdkVersion'
4 | import ConnectButton from './components/ConnectButton'
5 | import ConnectionSettings from './components/ConnectionSettings'
6 | import CopyUrlButton from './components/CopyUrlButton'
7 | import DatasetConnectionState from './components/DatasetConnectionState'
8 | import DisconnectButton from './components/DisconnectButton'
9 | import LocalVideo from './components/LocalVideo'
10 | import MediaSettings from './components/MediaSettings'
11 | import RemoteVideo from './components/RemoteVideo'
12 | import { useStore } from './store/useStore'
13 |
14 | const App: React.FC = () => {
15 | const setSettingsFromUrl = useStore((state) => state.setSettingsFromUrl)
16 |
17 | useEffect(() => {
18 | const params = new URLSearchParams(window.location.search)
19 | setSettingsFromUrl(params)
20 | }, [setSettingsFromUrl])
21 |
22 | return (
23 | <>
24 |
25 | Ayame Web SDK Version:
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | >
44 | )
45 | }
46 |
47 | export default App
48 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Pages Deploy
2 |
3 | on:
4 | push:
5 | branches: ["develop"]
6 |
7 | workflow_dispatch:
8 |
9 | jobs:
10 | deploy_job:
11 | runs-on: ubuntu-24.04
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: "22"
17 | - uses: pnpm/action-setup@v4
18 | with:
19 | version: 10
20 | - run: pnpm install
21 | - run: pnpm build
22 | - name: Build devtools
23 | run: pnpm build:devtools
24 | - name: Build typedoc
25 | run: pnpm build:typedoc
26 | - name: copy
27 | run: |
28 | mkdir -p ./output/devtools
29 | cp -r ./typedoc ./output/typedoc
30 | cp -r ./devtools/dist/* ./output/devtools
31 | cp -r ./devtools/dist/assets ./output/devtools/assets
32 | - name: Upload files
33 | uses: actions/upload-pages-artifact@v3
34 | with:
35 | path: ./output
36 | - name: Slack Notification
37 | if: failure()
38 | uses: rtCamp/action-slack-notify@v2
39 | env:
40 | SLACK_CHANNEL: media-processors
41 | SLACK_COLOR: danger
42 | SLACK_TITLE: Failure test
43 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
44 |
45 | deploy:
46 | needs: deploy_job
47 | permissions:
48 | pages: write
49 | id-token: write
50 | environment:
51 | name: github-pages
52 | url: ${{ steps.deployment.outputs.page_url }}
53 | runs-on: ubuntu-24.04
54 | steps:
55 | - name: Deploy to GitHub Pages
56 | id: deployment
57 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/biome.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
3 | "files": {
4 | "includes": ["**", "!**/dist/**"]
5 | },
6 | "assist": { "actions": { "source": { "organizeImports": "on" } } },
7 | "linter": {
8 | "enabled": true,
9 | "rules": {
10 | "recommended": true,
11 | "suspicious": {
12 | "noExplicitAny": "off"
13 | },
14 | "style": {
15 | "noParameterAssign": "error",
16 | "useAsConstAssertion": "error",
17 | "useDefaultParameterLast": "error",
18 | "useEnumInitializers": "error",
19 | "useSelfClosingElements": "error",
20 | "useSingleVarDeclarator": "error",
21 | "noUnusedTemplateLiteral": "error",
22 | "useNumberNamespace": "error",
23 | "noInferrableTypes": "error",
24 | "noUselessElse": "error"
25 | }
26 | }
27 | },
28 | "formatter": {
29 | "enabled": true,
30 | "formatWithErrors": false,
31 | "includes": ["**"],
32 | "indentStyle": "space",
33 | "indentWidth": 2,
34 | "lineWidth": 100
35 | },
36 | "json": {
37 | "parser": {
38 | "allowComments": true
39 | },
40 | "formatter": {
41 | "enabled": true,
42 | "indentStyle": "space",
43 | "indentWidth": 2,
44 | "lineWidth": 100
45 | }
46 | },
47 | "javascript": {
48 | "formatter": {
49 | "enabled": true,
50 | "quoteStyle": "single",
51 | "jsxQuoteStyle": "double",
52 | "trailingCommas": "all",
53 | "semicolons": "asNeeded",
54 | "arrowParentheses": "always",
55 | "indentStyle": "space",
56 | "indentWidth": 2,
57 | "lineWidth": 100,
58 | "quoteProperties": "asNeeded"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-ayame/ayame-web-sdk",
3 | "version": "2025.1.1",
4 | "description": "WebRTC Signaling Server Ayame Web SDK",
5 | "author": "Shiguredo Inc.",
6 | "license": "Apache-2.0",
7 | "homepage": "https://github.com/OpenAyame/ayame-web-sdk",
8 | "type": "module",
9 | "module": "dist/ayame.mjs",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/ayame.d.ts",
13 | "import": "./dist/ayame.mjs",
14 | "require": "./dist/ayame.mjs"
15 | }
16 | },
17 | "scripts": {
18 | "build": "vite build",
19 | "watch": "vite build --watch",
20 | "dev": "vite --config devtools/vite.config.mjs",
21 | "build:devtools": "vite build --config devtools/vite.config.mjs",
22 | "preview:devtools": "vite preview --config devtools/vite.config.mjs",
23 | "build:typedoc": "typedoc",
24 | "lint": "biome lint ./src",
25 | "fmt": "biome format --write ./src",
26 | "check": "tsc --noEmit",
27 | "dist": "pnpm clean && pnpm install && pnpm build",
28 | "clean": "git clean -ffdx -e .env.local -e .env",
29 | "test": "playwright test --project=chromium"
30 | },
31 | "bugs": {
32 | "url": "https://discord.gg/shiguredo"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/OpenAyame/ayame-web-sdk.git"
37 | },
38 | "directories": {
39 | "doc": "docs/"
40 | },
41 | "devDependencies": {
42 | "@biomejs/biome": "2.3.10",
43 | "@playwright/test": "1.57.0",
44 | "@types/node": "24.9.1",
45 | "@types/webrtc": "0.0.47",
46 | "typedoc": "0.28.15",
47 | "typescript": "5.9.3",
48 | "vite": "7.3.0",
49 | "vite-plugin-dts": "4.5.4"
50 | },
51 | "engines": {
52 | "node": ">=20",
53 | "pnpm": ">=10"
54 | },
55 | "files": [
56 | "dist"
57 | ]
58 | }
--------------------------------------------------------------------------------
/devtools/src/store/createPermissionSlice.ts:
--------------------------------------------------------------------------------
1 | import type { StateCreator } from 'zustand'
2 |
3 | export interface PermissionSlice {
4 | permissionState: {
5 | microphoneState: PermissionState | undefined
6 | cameraState: PermissionState | undefined
7 | }
8 |
9 | setMicrophonePermissionState: () => Promise
10 | setCameraPermissionState: () => Promise
11 | }
12 |
13 | export const createPermissionSlice: StateCreator = (set) => ({
14 | permissionState: {
15 | microphoneState: undefined,
16 | cameraState: undefined,
17 | },
18 |
19 | setMicrophonePermissionState: async () => {
20 | const permissionStatus = await navigator.permissions.query({
21 | name: 'microphone' as PermissionName,
22 | })
23 | set((state) => ({
24 | permissionState: {
25 | ...state.permissionState,
26 | microphoneState: permissionStatus.state,
27 | },
28 | }))
29 | // リアルタイムにパーミッションが変わったときに反映するようにする
30 | permissionStatus.onchange = () => {
31 | set((state) => ({
32 | permissionState: {
33 | ...state.permissionState,
34 | microphoneState: permissionStatus.state,
35 | },
36 | }))
37 | }
38 | },
39 |
40 | setCameraPermissionState: async () => {
41 | const permissionStatus = await navigator.permissions.query({
42 | name: 'camera' as PermissionName,
43 | })
44 | set((state) => ({
45 | permissionState: {
46 | ...state.permissionState,
47 | cameraState: permissionStatus.state,
48 | },
49 | }))
50 | // リアルタイムにパーミッションが変わったときに反映するようにする
51 | permissionStatus.onchange = () => {
52 | set((state) => ({
53 | permissionState: {
54 | ...state.permissionState,
55 | cameraState: permissionStatus.state,
56 | },
57 | }))
58 | }
59 | },
60 | })
61 |
--------------------------------------------------------------------------------
/devtools/src/components/VideoInputDevice.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const VideoInputDevice: React.FC = () => {
6 | const [devices, setDevices] = useState([])
7 | const setVideoInputDeviceId = useStore((state) => state.setVideoInputDeviceId)
8 | const videoInputDeviceId = useStore((state) => state.mediaDevice.videoInputDeviceId)
9 |
10 | useEffect(() => {
11 | const getDevices = async () => {
12 | const permissionStatus = await navigator.permissions.query({
13 | name: 'camera' as PermissionName,
14 | })
15 |
16 | const handlePermissionChange = async () => {
17 | if (permissionStatus.state === 'granted') {
18 | const devices = await navigator.mediaDevices.enumerateDevices()
19 | const videoInputDevices = devices.filter((device) => device.kind === 'videoinput')
20 | setDevices(videoInputDevices)
21 | } else {
22 | setDevices([])
23 | }
24 | }
25 |
26 | // 初期状態の処理
27 | handlePermissionChange()
28 |
29 | // 権限変更の監視
30 | permissionStatus.onchange = handlePermissionChange
31 |
32 | return () => {
33 | // ククリーンアップ
34 | permissionStatus.onchange = null
35 | }
36 | }
37 | getDevices()
38 | }, [])
39 |
40 | const handleChange = (e: React.ChangeEvent) => {
41 | setVideoInputDeviceId(e.target.value)
42 | }
43 |
44 | return (
45 |
52 | )
53 | }
54 |
55 | export default VideoInputDevice
56 |
--------------------------------------------------------------------------------
/devtools/src/components/AudioInputDevice.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const AudioInputDevice: React.FC = () => {
6 | const [devices, setDevices] = useState([])
7 | const setAudioInputDeviceId = useStore((state) => state.setAudioInputDeviceId)
8 | const audioInputDeviceId = useStore((state) => state.mediaDevice.audioInputDeviceId)
9 |
10 | useEffect(() => {
11 | const getDevices = async () => {
12 | const permissionStatus = await navigator.permissions.query({
13 | name: 'microphone' as PermissionName,
14 | })
15 |
16 | const handlePermissionChange = async () => {
17 | if (permissionStatus.state === 'granted') {
18 | const devices = await navigator.mediaDevices.enumerateDevices()
19 | const audioInputDevices = devices.filter((device) => device.kind === 'audioinput')
20 | setDevices(audioInputDevices)
21 | } else {
22 | setDevices([])
23 | }
24 | }
25 |
26 | // 初期状態の処理
27 | handlePermissionChange()
28 |
29 | // 権限変更の監視
30 | permissionStatus.onchange = handlePermissionChange
31 |
32 | return () => {
33 | // ククリーンアップ
34 | permissionStatus.onchange = null
35 | }
36 | }
37 | getDevices()
38 | }, [])
39 |
40 | const handleChange = (e: React.ChangeEvent) => {
41 | setAudioInputDeviceId(e.target.value)
42 | }
43 |
44 | return (
45 |
52 | )
53 | }
54 |
55 | export default AudioInputDevice
56 |
--------------------------------------------------------------------------------
/devtools/src/components/AudioOutputDevice.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const AudioOutputDevice: React.FC = () => {
6 | const [devices, setDevices] = useState([])
7 | const setAudioOutputDeviceId = useStore((state) => state.setAudioOutputDeviceId)
8 | const audioOutputDeviceId = useStore((state) => state.mediaDevice.audioOutputDeviceId)
9 |
10 | useEffect(() => {
11 | const getDevices = async () => {
12 | const permissionStatus = await navigator.permissions.query({
13 | name: 'microphone' as PermissionName,
14 | })
15 |
16 | const handlePermissionChange = async () => {
17 | if (permissionStatus.state === 'granted') {
18 | const devices = await navigator.mediaDevices.enumerateDevices()
19 | const audioOutputDevices = devices.filter((device) => device.kind === 'audiooutput')
20 | setDevices(audioOutputDevices)
21 | } else {
22 | setDevices([])
23 | }
24 | }
25 |
26 | // 初期状態の処理
27 | handlePermissionChange()
28 |
29 | // 権限変更の監視
30 | permissionStatus.onchange = handlePermissionChange
31 |
32 | return () => {
33 | // ククリーンアップ
34 | permissionStatus.onchange = null
35 | }
36 | }
37 | getDevices()
38 | }, [])
39 |
40 | const handleChange = (e: React.ChangeEvent) => {
41 | setAudioOutputDeviceId(e.target.value)
42 | }
43 |
44 | return (
45 |
52 | )
53 | }
54 |
55 | export default AudioOutputDevice
56 |
--------------------------------------------------------------------------------
/tests/devtools.test.ts:
--------------------------------------------------------------------------------
1 | import { version } from '@open-ayame/ayame-web-sdk'
2 | import { expect, test } from '@playwright/test'
3 |
4 | test('DevTools のテスト', async ({ browser }) => {
5 | const sendrecv1 = await browser.newPage()
6 | const sendrecv2 = await browser.newPage()
7 |
8 | await sendrecv1.goto('http://localhost:9000/')
9 | await sendrecv2.goto('http://localhost:9000/')
10 |
11 | // Ayame Web SDK のバージョンを確認
12 | await expect(sendrecv1.locator('[data-testid="ayame-web-sdk-version"]')).toHaveText(version(), {
13 | timeout: 10000,
14 | })
15 |
16 | // RoodID を取得
17 | const roomId1 = await sendrecv1.evaluate(() => {
18 | const roomIdElement = document.querySelector('[data-testid="room-id"]') as HTMLInputElement
19 | return roomIdElement.value
20 | })
21 | const roomId2 = await sendrecv2.evaluate(() => {
22 | const roomIdElement = document.querySelector('[data-testid="room-id"]') as HTMLInputElement
23 | return roomIdElement.value
24 | })
25 | const roomIdSuffix = crypto.randomUUID()
26 |
27 | // RoomId を再設定
28 | await sendrecv1.fill('[data-testid="room-id"]', `${roomId1}-${roomIdSuffix}`)
29 | await sendrecv2.fill('[data-testid="room-id"]', `${roomId2}-${roomIdSuffix}`)
30 |
31 | // ボタンが表示されるまで待つ
32 | await sendrecv1.waitForSelector('[data-testid="connect"]', { state: 'visible' })
33 | await sendrecv2.waitForSelector('[data-testid="connect"]', { state: 'visible' })
34 |
35 | await sendrecv1.click('[data-testid="connect"]')
36 | await sendrecv2.click('[data-testid="connect"]')
37 |
38 | // data-connection-state が connected になるまで待つ
39 | await expect(sendrecv1.locator('[data-testid="connection-state"]')).toHaveAttribute(
40 | 'data-connection-state',
41 | 'connected',
42 | { timeout: 10000 },
43 | )
44 |
45 | // もう一方のページも connected になるまで待つ
46 | await expect(sendrecv2.locator('[data-testid="connection-state"]')).toHaveAttribute(
47 | 'data-connection-state',
48 | 'connected',
49 | { timeout: 10000 },
50 | )
51 |
52 | await sendrecv1.click('[data-testid="disconnect"]')
53 | await sendrecv2.click('[data-testid="disconnect"]')
54 |
55 | await sendrecv1.close()
56 | await sendrecv2.close()
57 | })
58 |
--------------------------------------------------------------------------------
/devtools/src/components/MediaSettings.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useStore } from '../store/useStore'
3 | import AudioCodecMimeType from './AudioCodecMimeType'
4 | import AudioInputDevice from './AudioInputDevice'
5 | import AudioOutputDevice from './AudioOutputDevice'
6 | import AudioToggle from './AudioToggle'
7 | import CameraPermissionState from './CameraPermissionState'
8 | import MicrophonePermissionState from './MicrophonePermissionState'
9 | import RequestMediaPermissionButton from './RequestMediaPermissionButton'
10 | import TransceiverDirection from './TransceiverDirection'
11 | import VideoCodecMimeType from './VideoCodecMimeType'
12 | import VideoInputDevice from './VideoInputDevice'
13 | import VideoResolution from './VideoResolution'
14 | import VideoToggle from './VideoToggle'
15 |
16 | const MediaSettings: React.FC = () => {
17 | const audioDirection = useStore((state) => state.settings.audio.direction)
18 | const setAudioDirection = useStore((state) => state.setAudioDirection)
19 | const videoDirection = useStore((state) => state.settings.video.direction)
20 | const setVideoDirection = useStore((state) => state.setVideoDirection)
21 |
22 | return (
23 |
67 | )
68 | }
69 |
70 | export default MediaSettings
71 |
--------------------------------------------------------------------------------
/devtools/src/store/createAyameSlice.ts:
--------------------------------------------------------------------------------
1 | import type { Connection, version } from '@open-ayame/ayame-web-sdk'
2 | import type { StateCreator } from 'zustand'
3 |
4 | export interface AyameSlice {
5 | ayame: {
6 | version: string
7 | connection: Connection | null
8 | connectionState: RTCPeerConnectionState
9 | }
10 |
11 | mediaStream: {
12 | local: MediaStream | null
13 | remote: MediaStream | null
14 | }
15 |
16 | setAyameVersion: (version: string) => void
17 | setAyameConnection: (conn: Connection | null) => void
18 | setAyameConnectionState: (state: RTCPeerConnectionState) => void
19 |
20 | setLocalMediaStream: (stream: MediaStream | null) => void
21 | setRemoteMediaStream: (stream: MediaStream | null) => void
22 | }
23 |
24 | export const createAyameSlice: StateCreator = (set, get) => ({
25 | ayame: {
26 | version: '',
27 | connection: null,
28 | // とりあえず初期値なので new にしておく
29 | connectionState: 'new' as RTCPeerConnectionState,
30 | },
31 |
32 | mediaStream: {
33 | local: null,
34 | remote: null,
35 | },
36 |
37 | localMediaStream: null,
38 | remoteMediaStream: null,
39 |
40 | setAyameVersion: (version: string) => {
41 | set((state) => ({
42 | ayame: { ...state.ayame, version },
43 | }))
44 | },
45 |
46 | setAyameConnection: (conn: Connection | null) => {
47 | set((state) => ({
48 | ayame: { ...state.ayame, connection: conn },
49 | }))
50 | },
51 |
52 | setAyameConnectionState: (connectionState: RTCPeerConnectionState) => {
53 | set((state) => ({
54 | ayame: { ...state.ayame, connectionState: connectionState },
55 | }))
56 | },
57 |
58 | setLocalMediaStream: (stream: MediaStream | null) => {
59 | set((state) => ({
60 | mediaStream: { ...state.mediaStream, local: stream },
61 | }))
62 | },
63 | setRemoteMediaStream: (stream: MediaStream | null) => {
64 | set((state) => ({
65 | mediaStream: { ...state.mediaStream, remote: stream },
66 | }))
67 | },
68 | })
69 |
70 | // デバッグ用の subscribe 設定
71 | // ストアの作成後に subscribe を設定
72 | // useAyameStore.subscribe((state, prevState) => {
73 | // if (state.localMediaStream !== prevState.localMediaStream) {
74 | // console.log('localMediaStream changed:', {
75 | // from: prevState.localMediaStream,
76 | // to: state.localMediaStream,
77 | // stack: new Error().stack,
78 | // })
79 | // }
80 | // if (state.remoteMediaStream !== prevState.remoteMediaStream) {
81 | // console.log('remoteMediaStream changed:', {
82 | // from: prevState.remoteMediaStream,
83 | // to: state.remoteMediaStream,
84 | // stack: new Error().stack,
85 | // })
86 | // }
87 | // })
88 |
--------------------------------------------------------------------------------
/tests/codec.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test'
2 |
3 | const codecs = [
4 | // 音声は Opus のみ
5 | // 映像は H.264/H.265 は Playwright ではサポートされていない
6 | { audioCodec: 'audio/opus', videoCodec: 'video/AV1' },
7 | { audioCodec: 'audio/opus', videoCodec: 'video/VP8' },
8 | { audioCodec: 'audio/opus', videoCodec: 'video/VP9' },
9 | ]
10 |
11 | test.describe
12 | .parallel('コーデックテスト', () => {
13 | for (const { audioCodec, videoCodec } of codecs) {
14 | test(`DevTools with ${audioCodec} and ${videoCodec}`, async ({ browser }) => {
15 | const sendrecv1 = await browser.newPage()
16 | const sendrecv2 = await browser.newPage()
17 |
18 | await sendrecv1.goto('http://localhost:9000/')
19 | await sendrecv2.goto('http://localhost:9000/')
20 |
21 | // RoodID を取得
22 | const roomId1 = await sendrecv1.evaluate(() => {
23 | const roomIdElement = document.querySelector(
24 | '[data-testid="room-id"]',
25 | ) as HTMLInputElement
26 | return roomIdElement.value
27 | })
28 | const roomId2 = await sendrecv2.evaluate(() => {
29 | const roomIdElement = document.querySelector(
30 | '[data-testid="room-id"]',
31 | ) as HTMLInputElement
32 | return roomIdElement.value
33 | })
34 | const roomIdSuffix = crypto.randomUUID()
35 |
36 | // RoomId を再設定
37 | await sendrecv1.fill('[data-testid="room-id"]', `${roomId1}-${roomIdSuffix}`)
38 | await sendrecv2.fill('[data-testid="room-id"]', `${roomId2}-${roomIdSuffix}`)
39 |
40 | // 音声コーデックを設定
41 | await sendrecv1.selectOption('[data-testid="audio-codec-mime-type"]', audioCodec)
42 |
43 | // 映像コーデックを設定
44 | await sendrecv1.selectOption('[data-testid="video-codec-mime-type"]', videoCodec)
45 |
46 | // ボタンが表示されるまで待つ
47 | await sendrecv1.waitForSelector('[data-testid="connect"]', { state: 'visible' })
48 | await sendrecv2.waitForSelector('[data-testid="connect"]', { state: 'visible' })
49 |
50 | await sendrecv1.click('[data-testid="connect"]')
51 | await sendrecv2.click('[data-testid="connect"]')
52 |
53 | // data-connection-state が connected になるまで待つ
54 | await expect(sendrecv1.locator('[data-testid="connection-state"]')).toHaveAttribute(
55 | 'data-connection-state',
56 | 'connected',
57 | { timeout: 10000 },
58 | )
59 |
60 | // もう一方のページも connected になるまで待つ
61 | await expect(sendrecv2.locator('[data-testid="connection-state"]')).toHaveAttribute(
62 | 'data-connection-state',
63 | 'connected',
64 | { timeout: 10000 },
65 | )
66 |
67 | await sendrecv1.click('[data-testid="disconnect"]')
68 | await sendrecv2.click('[data-testid="disconnect"]')
69 |
70 | await sendrecv1.close()
71 | await sendrecv2.close()
72 | })
73 | }
74 | })
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebRTC Signaling Server Ayame Web SDK
2 |
3 | [](https://badge.fury.io/js/%40open-ayame%2Fayame-web-sdk)
4 | [](https://opensource.org/licenses/Apache-2.0)
5 | [](https://github.com/OpenAyame/ayame-web-sdk/actions)
6 |
7 | ## About Shiguredo's open source software
8 |
9 | We will not respond to PRs or issues that have not been discussed on Discord. Also, Discord is only available in Japanese.
10 |
11 | Please read before use.
12 |
13 | ## 時雨堂のオープンソースソフトウェアについて
14 |
15 | 利用前に をお読みください。
16 |
17 | ## 概要
18 |
19 | [WebRTC Signaling Server Ayame](https://github.com/OpenAyame/ayame) をブラウザから利用する SDK です。
20 |
21 | ## 使い方
22 |
23 | ### npm
24 |
25 | ```bash
26 | npm install @open-ayame/ayame-web-sdk
27 | ```
28 |
29 | ### pnpm
30 |
31 | ```bash
32 | pnpm add @open-ayame/ayame-web-sdk
33 | ```
34 |
35 | ## 動作環境
36 |
37 | 最新のブラウザを利用してください。
38 |
39 | - Google Chrome
40 | - Apple Safari
41 | - Mozilla Firefox
42 | - Microsoft Edge
43 |
44 | ## Ayame DevTools
45 |
46 | Ayame Web SDK を利用した、開発ツールです。
47 |
48 | - [Ayame Labo](https://ayame-labo.shiguredo.app/) を利用する例です
49 | - `GitHub ログイン名@ayame-devtools` というルーム ID にしていますが、ルーム名の `ayame-devtools` は任意の文字列に変更できます
50 | - シグナリングキーは [Ayame Labo](https://ayame-labo.shiguredo.app/) のダッシュボード上で取得してください
51 |
52 | ```bash
53 | # cp .env.template .env.local
54 | VITE_AYAME_SIGNALING_URL=wss://ayame-labo.shiguredo.app/signaling
55 | VITE_AYAME_ROOM_ID_PREFIX={GitHubログイン名}@
56 | VITE_AYAME_ROOM_NAME=ayame-devtools
57 | VITE_AYAME_SIGNALING_KEY={シグナリングキー}
58 | ```
59 |
60 | ```bash
61 | pnpm install
62 | pnpm build
63 | pnpm dev
64 | ```
65 |
66 | にアクセスすると、以下のような画面が表示されます。
67 |
68 | [](https://gyazo.com/69b1e9529f1a0e26bd3bcd3b74c95021)
69 |
70 | この画面を 2 つタブで開いて、 `Connect` ボタンを押して映像が双方向に表示されたら成功です。
71 |
72 | ### オンライン Ayame DevTools
73 |
74 | 以下から利用できます。
75 |
76 |
77 |
78 | ## 最小限のサンプル
79 |
80 | [OpenAyame/ayame-web-sdk-examples](https://github.com/OpenAyame/ayame-web-sdk-examples) に最小限のサンプルコードを用意しています。
81 |
82 | ## API ドキュメント
83 |
84 | API ドキュメントは以下の URL を参照してください。
85 |
86 |
87 |
88 | ## ライセンス
89 |
90 | Apache License 2.0
91 |
92 | ```text
93 | Copyright 2019-2025, Shiguredo Inc.
94 |
95 | Licensed under the Apache License, Version 2.0 (the "License");
96 | you may not use this file except in compliance with the License.
97 | You may obtain a copy of the License at
98 |
99 | http://www.apache.org/licenses/LICENSE-2.0
100 |
101 | Unless required by applicable law or agreed to in writing, software
102 | distributed under the License is distributed on an "AS IS" BASIS,
103 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
104 | See the License for the specific language governing permissions and
105 | limitations under the License.
106 | ```
107 |
--------------------------------------------------------------------------------
/devtools/src/components/RequestMediaPermissionButton.tsx:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useState } from 'react'
3 | import { useStore } from '../store/useStore'
4 |
5 | const RequestMediaPermissionButton: React.FC<{
6 | buttonText?: string
7 | }> = ({ buttonText = 'Request media permission' }) => {
8 | const isAudioEnabled = useStore((state) => state.settings.audio.isEnable)
9 | const isVideoEnabled = useStore((state) => state.settings.video.isEnable)
10 | const videoResolution = useStore((state) => state.settings.video.resolution)
11 | const [isPermissionsGranted, setIsPermissionsGranted] = useState(false)
12 |
13 | useEffect(() => {
14 | const checkPermissions = async () => {
15 | // チェックすべきパーミッションを入れる
16 | const PermissionsToCheck = []
17 |
18 | // 音声が有効だったらパーミッションのチェックをする
19 | if (isAudioEnabled) {
20 | const microphonePermission = await navigator.permissions.query({
21 | name: 'microphone' as PermissionName,
22 | })
23 | PermissionsToCheck.push(microphonePermission)
24 | microphonePermission.onchange = () => {
25 | // パーミッションが granted でなければボタンを有効にする
26 | setIsPermissionsGranted(microphonePermission.state === 'granted')
27 | }
28 | }
29 | // 映像が有効だったらパーミッションのチェックをする
30 | if (isVideoEnabled) {
31 | const cameraPermission = await navigator.permissions.query({
32 | name: 'camera' as PermissionName,
33 | })
34 | PermissionsToCheck.push(cameraPermission)
35 | cameraPermission.onchange = () => {
36 | // パーミッションが granted でなければボタンを有効にする
37 | setIsPermissionsGranted(cameraPermission.state === 'granted')
38 | }
39 | }
40 |
41 | // パーミッションをチェックする必要がなかったら終了
42 | if (PermissionsToCheck.length === 0) {
43 | setIsPermissionsGranted(true)
44 | return
45 | }
46 |
47 | // パーミッションのチェックをする
48 | const allGranted = PermissionsToCheck.every((permission) => permission.state === 'granted')
49 | setIsPermissionsGranted(allGranted)
50 | }
51 | checkPermissions()
52 | }, [isAudioEnabled, isVideoEnabled])
53 |
54 | const handleClick = async () => {
55 | try {
56 | // ちゃんと有効にしているデバイスのパーミッションだけを取りに行く
57 | let videoConstraints: boolean | MediaTrackConstraints = isVideoEnabled
58 | if (isVideoEnabled && videoResolution && videoResolution !== 'undefined') {
59 | const [width, height] = videoResolution.split('x').map(Number)
60 | if (width && height) {
61 | videoConstraints = {
62 | width: { ideal: width },
63 | height: { ideal: height },
64 | }
65 | }
66 | }
67 | const constraints = {
68 | audio: isAudioEnabled,
69 | video: videoConstraints,
70 | }
71 | // メディアデバイスのパーミッションを取りに行く
72 | const stream = await navigator.mediaDevices.getUserMedia(constraints)
73 | // ストリームを停止する
74 | for (const track of stream.getTracks()) {
75 | track.stop()
76 | }
77 | } catch (error) {
78 | console.error('Failed to get media devices:', error)
79 | }
80 | }
81 |
82 | // を利用した microphone/camera の権限取得
83 | if ('HTMLPermissionElement' in window) {
84 | // @ts-ignore HTMLPermissionElement を認識しないため
85 | return
86 | }
87 |
88 | return (
89 |
92 | )
93 | }
94 |
95 | export default RequestMediaPermissionButton
96 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @ignore
3 | */
4 | interface Window {
5 | performance: WindowPerformance
6 | navigator: any
7 | }
8 | interface WindowPerformance {
9 | now(): number
10 | }
11 | declare let window: Window
12 |
13 | /**
14 | * ブラウザを判定する
15 | */
16 | export function browser(): string {
17 | const ua = window.navigator.userAgent.toLocaleLowerCase()
18 | if (ua.indexOf('edge') !== -1) {
19 | return 'edge'
20 | }
21 | if (ua.indexOf('chrome') !== -1 && ua.indexOf('edge') === -1) {
22 | return 'chrome'
23 | }
24 | if (ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1) {
25 | return 'safari'
26 | }
27 | if (ua.indexOf('opera') !== -1) {
28 | return 'opera'
29 | }
30 | if (ua.indexOf('firefox') !== -1) {
31 | return 'firefox'
32 | }
33 | return 'unknown'
34 | }
35 |
36 | /**
37 | * デバッグログを出力する
38 | */
39 | export function traceLog(title: string, value?: string | Record): void {
40 | let prefix = ''
41 | if (window.performance) {
42 | prefix = `[Ayame ${(window.performance.now() / 1000).toFixed(3)}]`
43 | }
44 | if (browser() === 'edge') {
45 | console.log(`${prefix} ${title}\n`, value)
46 | } else {
47 | console.info(`${prefix} ${title}\n`, value)
48 | }
49 | }
50 |
51 | /**
52 | * 指定された codec にマッチする codec のリストを返す
53 | * リストなのはプロファイルが複数合ったり、 RTX, RED, ULPFEC などの codec も含めるため
54 | */
55 | export const getSelectedCodecs = (
56 | kind: 'audio' | 'video',
57 | selectedCodecMimeType: string,
58 | codecs: RTCRtpCodecCapability[],
59 | ): RTCRtpCodecCapability[] => {
60 | const filteredCodecs = codecs.filter((c) => {
61 | const codecMimeType = c.mimeType.toLowerCase()
62 |
63 | // 指定された codec はマッチしたら true
64 | if (codecMimeType === selectedCodecMimeType.toLowerCase()) {
65 | return true
66 | }
67 |
68 | // rtx, red, ulpfec は常に true にする
69 | if (
70 | codecMimeType === `${kind}/rtx` ||
71 | codecMimeType === `${kind}/red` ||
72 | codecMimeType === `${kind}/ulpfec`
73 | ) {
74 | return true
75 | }
76 |
77 | return false
78 | })
79 | return filteredCodecs
80 | }
81 |
82 | /**
83 | * 利用可能な映像のコーデックを取得する
84 | */
85 | export const getAvailableCodecs = (
86 | kind: 'audio' | 'video',
87 | direction: 'sender' | 'receiver',
88 | ): string[] => {
89 | if (typeof RTCRtpSender === 'undefined' || typeof RTCRtpReceiver === 'undefined') {
90 | return []
91 | }
92 |
93 | // sendrecv と sendonly は RTCRtpSender を使う
94 | // recvonly は RTCRtpReceiver を使う
95 | const getCapabilities =
96 | direction === 'sender' || direction === 'receiver'
97 | ? RTCRtpSender.getCapabilities
98 | : RTCRtpReceiver.getCapabilities
99 |
100 | if (typeof getCapabilities !== 'function') {
101 | return []
102 | }
103 |
104 | const codecs = getCapabilities(kind)?.codecs
105 | if (!codecs) {
106 | return []
107 | }
108 |
109 | return (
110 | codecs
111 | .filter((c) => {
112 | // mimeType は insensitive-case なので lowerCase に変換する
113 | const codecType = c.mimeType.toLowerCase()
114 |
115 | // rtx/red/ulpfec はフィルターとして削除する
116 | if (
117 | codecType === `${kind}/rtx` ||
118 | codecType === `${kind}/red` ||
119 | codecType === `${kind}/ulpfec`
120 | ) {
121 | return false
122 | }
123 |
124 | return true
125 | })
126 | // mimeType が既に存在している場合は重複を削除する
127 | .filter((c, index, self) => index === self.findIndex((t) => t.mimeType === c.mimeType))
128 | .map((c) => c.mimeType)
129 | .sort()
130 | )
131 | }
132 |
--------------------------------------------------------------------------------
/devtools/src/components/ConnectButton.tsx:
--------------------------------------------------------------------------------
1 | import { createConnection, defaultOptions } from '@open-ayame/ayame-web-sdk'
2 | import type { AyameAddStreamEvent } from '@open-ayame/ayame-web-sdk'
3 | import { useStore } from '../store/useStore'
4 |
5 | import type React from 'react'
6 | const ConnectButton: React.FC = () => {
7 | const audioEnabled = useStore((state) => state.settings.audio.isEnable)
8 | const audioDirection = useStore((state) => state.settings.audio.direction)
9 | const audioCodecMimeType = useStore((state) => state.settings.audio.codecMimeType)
10 | const videoEnabled = useStore((state) => state.settings.video.isEnable)
11 | const videoDirection = useStore((state) => state.settings.video.direction)
12 | const videoCodecMimeType = useStore((state) => state.settings.video.codecMimeType)
13 | const videoResolution = useStore((state) => state.settings.video.resolution)
14 |
15 | const signalingUrl = useStore((state) => state.settings.signalingUrl)
16 | const roomId = useStore((state) => state.settings.roomId)
17 | const debug = useStore((state) => state.settings.debug)
18 | const signalingKey = useStore((state) => state.settings.signalingKey)
19 |
20 | const setAyameConnection = useStore((state) => state.setAyameConnection)
21 | const setLocalMediaStream = useStore((state) => state.setLocalMediaStream)
22 | const setRemoteMediaStream = useStore((state) => state.setRemoteMediaStream)
23 | const setAyameConnectionState = useStore((state) => state.setAyameConnectionState)
24 |
25 | const handleClick = async () => {
26 | const options = defaultOptions
27 | options.audio.enabled = audioEnabled
28 | options.audio.direction = audioDirection
29 | options.audio.codecMimeType = audioCodecMimeType
30 | options.video.enabled = videoEnabled
31 | options.video.direction = videoDirection
32 | options.video.codecMimeType = videoCodecMimeType
33 | options.signalingKey = signalingKey
34 |
35 | const conn = createConnection(signalingUrl, roomId, options, debug)
36 |
37 | let localStream: MediaStream | null = null
38 |
39 | if (
40 | (audioEnabled && audioDirection !== 'recvonly') ||
41 | (videoEnabled && videoDirection !== 'recvonly')
42 | ) {
43 | let videoConstraints: boolean | MediaTrackConstraints = videoEnabled
44 | if (videoEnabled && videoResolution && videoResolution !== 'undefined') {
45 | const [width, height] = videoResolution.split('x').map(Number)
46 | if (width && height) {
47 | videoConstraints = {
48 | width: { ideal: width },
49 | height: { ideal: height },
50 | }
51 | }
52 | }
53 | localStream = await navigator.mediaDevices.getUserMedia({
54 | audio: audioEnabled,
55 | video: videoConstraints,
56 | })
57 | setLocalMediaStream(localStream)
58 | }
59 |
60 | conn.on('addstream', (event: AyameAddStreamEvent) => {
61 | setRemoteMediaStream(event.stream)
62 | })
63 |
64 | conn.on('open', () => {
65 | const pc = conn.peerConnection
66 | if (!pc) {
67 | return
68 | }
69 | pc.onconnectionstatechange = (event) => {
70 | setAyameConnectionState(pc.connectionState)
71 | }
72 | })
73 |
74 | // 切断時にローカルとリモートのメディアストリームを停止する
75 | conn.on('disconnect', () => {
76 | // この関数内で取得した localStream を停止する
77 | // store を経由しないようにする
78 | if (localStream) {
79 | for (const track of localStream.getTracks()) {
80 | track.stop()
81 | }
82 | }
83 |
84 | setLocalMediaStream(null)
85 | setRemoteMediaStream(null)
86 | setAyameConnection(null)
87 | })
88 |
89 | await conn.connect(localStream)
90 |
91 | setAyameConnection(conn)
92 | }
93 |
94 | return (
95 |
98 | )
99 | }
100 |
101 | export default ConnectButton
102 |
--------------------------------------------------------------------------------
/devtools/src/store/signals.ts:
--------------------------------------------------------------------------------
1 | import { signal, computed } from '@preact/signals'
2 | import type { Connection, Direction } from '@open-ayame/ayame-web-sdk'
3 |
4 | // Ayame signals
5 | export const ayameVersion = signal('')
6 | export const ayameConnection = signal(null)
7 | export const ayameConnectionState = signal('new')
8 |
9 | // Media stream signals
10 | export const localMediaStream = signal(null)
11 | export const remoteMediaStream = signal(null)
12 |
13 | // Permission signals
14 | export const microphonePermissionState = signal(undefined)
15 | export const cameraPermissionState = signal(undefined)
16 |
17 | // Media device signals
18 | export const audioInputDeviceId = signal('default')
19 | export const audioOutputDeviceId = signal('default')
20 | export const videoInputDeviceId = signal('default')
21 |
22 | // Settings signals
23 | export const audioEnabled = signal(true)
24 | export const audioDirection = signal('sendrecv')
25 | export const audioCodecMimeType = signal('undefined')
26 |
27 | export const videoEnabled = signal(true)
28 | export const videoDirection = signal('sendrecv')
29 | export const videoCodecMimeType = signal('undefined')
30 |
31 | export const signalingUrl = signal(import.meta.env.VITE_AYAME_SIGNALING_URL || '')
32 | export const roomId = signal(
33 | `${import.meta.env.VITE_AYAME_ROOM_ID_PREFIX ?? ''}${import.meta.env.VITE_AYAME_ROOM_NAME ?? ''}` || ''
34 | )
35 | export const clientId = signal(crypto.randomUUID())
36 | export const signalingKey = signal(import.meta.env.VITE_AYAME_SIGNALING_KEY || '')
37 |
38 | export const debug = signal(false)
39 | export const standalone = signal(false)
40 |
41 | // Helper functions for permission states
42 | export const setMicrophonePermissionState = async () => {
43 | const permissionStatus = await navigator.permissions.query({
44 | name: 'microphone' as PermissionName,
45 | })
46 | microphonePermissionState.value = permissionStatus.state
47 |
48 | // リアルタイムにパーミッションが変わったときに反映するようにする
49 | permissionStatus.onchange = () => {
50 | microphonePermissionState.value = permissionStatus.state
51 | }
52 | }
53 |
54 | export const setCameraPermissionState = async () => {
55 | const permissionStatus = await navigator.permissions.query({
56 | name: 'camera' as PermissionName,
57 | })
58 | cameraPermissionState.value = permissionStatus.state
59 |
60 | // リアルタイムにパーミッションが変わったときに反映するようにする
61 | permissionStatus.onchange = () => {
62 | cameraPermissionState.value = permissionStatus.state
63 | }
64 | }
65 |
66 | // Helper functions for URL params
67 | export const generateUrlParams = () => {
68 | const params = new URLSearchParams()
69 |
70 | params.set('audio', audioEnabled.value.toString())
71 | params.set('audioDirection', audioDirection.value)
72 | params.set('audioCodecMimeType', audioCodecMimeType.value)
73 |
74 | params.set('video', videoEnabled.value.toString())
75 | params.set('videoDirection', videoDirection.value)
76 | params.set('videoCodecMimeType', videoCodecMimeType.value)
77 |
78 | params.set('signalingUrl', signalingUrl.value)
79 | params.set('roomId', roomId.value)
80 | params.set('signalingKey', signalingKey.value)
81 | params.set('debug', debug.value.toString())
82 | params.set('standalone', standalone.value.toString())
83 |
84 | return params.toString()
85 | }
86 |
87 | export const setSettingsFromUrl = (params: URLSearchParams) => {
88 | audioEnabled.value = params.get('audio') !== 'false'
89 | audioDirection.value = (params.get('audioDirection') as Direction) || 'sendrecv'
90 | audioCodecMimeType.value = params.get('audioCodecMimeType') || 'undefined'
91 |
92 | videoEnabled.value = params.get('video') !== 'false'
93 | videoDirection.value = (params.get('videoDirection') as Direction) || 'sendrecv'
94 | videoCodecMimeType.value = params.get('videoCodecMimeType') || 'undefined'
95 |
96 | // 項目がなかった場合は今ある値をそのまま利用する
97 | signalingUrl.value = params.get('signalingUrl') || signalingUrl.value
98 | roomId.value = params.get('roomId') || roomId.value
99 | clientId.value = params.get('clientId') || clientId.value
100 | signalingKey.value = params.get('signalingKey') || signalingKey.value
101 | debug.value = params.get('debug') === 'true'
102 | standalone.value = params.get('standalone') === 'true'
103 | }
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # リリースノート
2 |
3 | - CHANGE
4 | - 下位互換のない変更
5 | - UPDATE
6 | - 下位互換がある変更
7 | - ADD
8 | - 下位互換がある追加
9 | - FIX
10 | - バグ修正
11 |
12 | ## develop
13 |
14 | - [ADD] DevTools に解像度を設定するオプションを追加
15 | - @voluntas
16 |
17 | ## 2025.1.1
18 |
19 | - [FIX] Ayame Web SDK のバージョンを取得できなかったのを修正する
20 | - @voluntas
21 |
22 | ### misc
23 |
24 | - [ADD] Ayame Web SDK のバージョンを確認する E2E テストを追加
25 | - @voluntas
26 |
27 | ## 2025.1.0
28 |
29 | - [CHANGE] `ConnectionVideoOption` の `codec` を `codecMimeType` へ変更する
30 | - @voluntas
31 | - [CHANGE] `removestream` コールバックは利用されていないため廃止する
32 | - @voluntas
33 | - [UPDATE] `setCodecPreferences` を利用してコーデックを指定できるようにする
34 | - @voluntas
35 | - [ADD] `AyameAddStreamEvent` を追加する
36 | - `addstream` コールバックのイベント型を `AyameAddStreamEvent` として追加する
37 | - @voluntas
38 | - [ADD] `standalone` モードに対応する
39 | - `options` に `standalone` を追加する
40 | - `standalone` モード時は、接続完了時に ayame に `type: connected` を送信する
41 | - `standalone` モード時は、ayame から WebSocket 接続が切断されても、ブラウザ間の接続は維持する
42 | - @Hexa
43 | - [FIX] `disconnect` の処理が正常に動作しない問題を修正する
44 | - @voluntas
45 |
46 | ### misc
47 |
48 | - [CHANGE] ConnectionBase を Connection へ変更する
49 | - @voluntas
50 | - [CHANGE] rollup から [Vite](https://vite.dev/) へ変更
51 | - @voluntas
52 | - [CHANGE] npm から [pnpm](https://pnpm.io/) に変更
53 | - @voluntas
54 | - [CHANGE] eslint から [biome](https://biomejs.dev/) へ変更
55 | - @voluntas
56 | - [CHANGE] prettier から [biome](https://biomejs.dev/) へ変更する
57 | - @voluntas
58 | - [CHANGE] GitHub Actions の node-version を 20 と 22 にする
59 | - @voluntas
60 | - [UPDATE] ubuntu-latest から ubuntu-24.04 に変更する
61 | - @voluntas
62 | - [ADD] 検証用の Ayame DevTools を追加
63 | - @voluntas
64 | - [ADD] Playwright と Ayame DevTools を利用した E2E テストを追加
65 | - @voluntas
66 |
67 | ## 2022.1
68 |
69 | - [CHANGE] packege.json の devDependencies を最新へ追従する
70 | - `rollup` を `^2.66.1` へ上げる
71 | - `rollup-plugin-terser` を `^7.0.2` へ上げる
72 | - `@rollup/plugin-node-resolve` を `^13.1.3` に変更する
73 | - `@rollup/plugin-typescript` を `^8.3.0` に変更する
74 | - `typescript` を `^4.5.5` に上げる
75 | - `@typescript-eslint/eslint-plugin` を `^5.10.` に上げる
76 | - `@typescript-eslint/parse` を `^5.10.` に上げる
77 | - `@types/node` を `^16.11.7` へ上げる
78 | - `@types/webrtc` を `^0.0.31` へ上げる
79 | - `eslint` を `^8.8.0` に上げる
80 | - `eslint-config-prettier` を `^8.3.0` に上げる
81 | - `eslint-plugin-import` を `^2.25.4` に上げる
82 | - @voluntas
83 | - [CHANGE] esdoc を削除
84 | - @voluntas
85 | - [CHANGE] yarn の利用をやめ npm に切り替える
86 | - @voluntas
87 | - [CHANGE] `.eslintrc.js` から `prettier/@typescript-eslint` を削除
88 | - @voluntas
89 | - [CHANGE] GitHub Actions の node-version を 16 固定にする
90 | - @voluntas
91 | - [CHANGE] Google STUN サーバを削除
92 | - @voluntas
93 | - [CHANGE] tsconfig.json の設定を変更
94 | - target / module を es2020 へ変更
95 | - newLine を追加
96 | - declarationDir を追加
97 | - @voluntas
98 | - [UPDATE] rollup.config.js の設定を変更
99 | - sourceMap を sourcemap へ変更
100 | - entry を削除
101 | - rollup-plugin-node-resolve を @rollup/plugin-node-resolve へ変更
102 | - rollup-plugin-typescript2 を @rollup/plugin-typescript へ変更
103 | - format: 'module' で mjs を出力する
104 | - @voluntas
105 | - [UPDATE] GitHub Actions の actions/checkout を v2 に上げる
106 | - @voluntas
107 | - [ADD] `.prettierrc.json` を追加
108 | - @voluntas
109 | - [ADD] VideoCodecOption に `AV1` と `H.265` を追加
110 | - @voluntas
111 | - [ADD] npm run doc コマンド追加
112 | - TypeDoc により apidoc/ に出力
113 | - @voluntas
114 |
115 | ## 2020.3
116 |
117 | - [ADD] TypeScript の型定義ファイルを出力するようにする
118 | - @horiuchi
119 |
120 | ## 2020.2.1
121 |
122 | - [ADD] ayame.min.js / ayame.js を 2020.2.1 にアップデート
123 |
124 | ## 2020.2
125 |
126 | **DataChannel 関連で下位互換性がなくなっていますので注意してください**
127 |
128 | - [CHANGE] addDataChannel, sendData を削除する
129 | - @Hexa
130 | - [CHANGE] on('data') コールバックを削除する
131 | - @Hexa
132 | - [ADD] createDataChannel を追加する
133 | - @Hexa
134 | - [ADD] on('datachannel') コールバックを追加する
135 | - @Hexa
136 | - [FIX] offer 側の場合のみ RTCDataChannel オブジェクトを作成するように修正する
137 | - @Hexa
138 | - [CHANGE] Ayame が isExistUser を送ってくる場合のみ接続できるようにする
139 | - @Hexa
140 | - [FIX] bye を受信した場合にも on('disconnect') コールバックが発火するように修正する
141 | - @Hexa
142 |
143 | ## 2020.1.2
144 |
145 | - [FIX] 依存ライブラリを最新にする
146 | - @voluntas
147 |
148 | ## 2020.1.1
149 |
150 | - [FIX] on('disconnect') コールバックが発火するように修正する
151 | - @Hexa
152 |
153 | ## 2020.1.0
154 |
155 | **リリース番号フォーマットを変更しました**
156 |
157 | - [FIX] 再度の接続時にオブジェクトを作成しないようにする
158 | - @Hexa
159 | - [FIX] 切断時の他方の切断処理をエラーにならないように修正する
160 | - @Hexa
161 | - [UPDATE] close 待ち間隔を 400ms に変更する
162 | - @Hexa
163 | - [UPDATE] テストの整理
164 | - @Hexa
165 |
--------------------------------------------------------------------------------
/canary.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import re
3 | import subprocess
4 | from typing import Optional
5 |
6 |
7 | # ファイルを読み込み、バージョンを更新
8 | def update_version(file_path: str, dry_run: bool) -> Optional[str]:
9 | with open(file_path, "r", encoding="utf-8") as f:
10 | content: str = f.read()
11 |
12 | # 現在のバージョンを取得
13 | current_version_match = re.search(r'"version"\s*:\s*"([\d\.\w-]+)"', content)
14 | if not current_version_match:
15 | raise ValueError("Version not found or incorrect format in package.json")
16 |
17 | current_version: str = current_version_match.group(1)
18 |
19 | # バージョンが -canary.X を持っている場合の更新
20 | if "-canary." in current_version:
21 | new_content, count = re.subn(
22 | r'("version"\s*:\s*")(\d+\.\d+\.\d+-canary\.)(\d+)',
23 | lambda m: f"{m.group(1)}{m.group(2)}{int(m.group(3)) + 1}",
24 | content,
25 | )
26 | else:
27 | # -canary.X がない場合、次のマイナーバージョンにして -canary.0 を追加
28 | new_content, count = re.subn(
29 | r'("version"\s*:\s*")(\d+)\.(\d+)\.(\d+)',
30 | lambda m: f"{m.group(1)}{m.group(2)}.{int(m.group(3)) + 1}.0-canary.0",
31 | content,
32 | )
33 |
34 | if count == 0:
35 | raise ValueError("Version not found or incorrect format in package.json")
36 |
37 | # 新しいバージョンを確認
38 | new_version_match = re.search(r'"version"\s*:\s*"([\d\.\w-]+)"', new_content)
39 | if not new_version_match:
40 | raise ValueError("Failed to extract the new version after the update.")
41 |
42 | new_version: str = new_version_match.group(1)
43 |
44 | print(f"Current version: {current_version}")
45 | print(f"New version: {new_version}")
46 | confirmation: str = (
47 | input("Do you want to update the version? (Y/n): ").strip().lower()
48 | )
49 |
50 | if confirmation != "y":
51 | print("Version update canceled.")
52 | return None
53 |
54 | # Dry-run 時の動作
55 | if dry_run:
56 | print("Dry-run: Version would be updated to:")
57 | print(new_content)
58 | else:
59 | with open(file_path, "w", encoding="utf-8") as f:
60 | f.write(new_content)
61 | print(f"Version updated in package.json to {new_version}")
62 |
63 | return new_version
64 |
65 |
66 | # pnpm dist の実行
67 | def run_pnpm_operations(dry_run: bool) -> None:
68 | if dry_run:
69 | print("Dry-run: Would run 'pnpm dist'")
70 | else:
71 | subprocess.run(["pnpm", "dist"], check=True)
72 | print("pnpm dist executed")
73 |
74 |
75 | # git コミット、タグ、プッシュを実行
76 | def git_commit_version(new_version: str, dry_run: bool) -> None:
77 | if dry_run:
78 | print("Dry-run: Would run 'git add package.json'")
79 | print(f"Dry-run: Would run '[canary] Bump version to {new_version}'")
80 | else:
81 | subprocess.run(["git", "add", "package.json"], check=True)
82 | subprocess.run(
83 | ["git", "commit", "-m", f"[canary] Bump version to {new_version}"],
84 | check=True,
85 | )
86 | print(f"Version bumped and committed: {new_version}")
87 |
88 |
89 | # git コミット、タグ、プッシュを実行
90 | def git_operations_after_build(new_version: str, dry_run: bool) -> None:
91 | if dry_run:
92 | print(f"Dry-run: Would run 'git tag {new_version}'")
93 | print("Dry-run: Would run 'git push'")
94 | print(f"Dry-run: Would run 'git push origin {new_version}'")
95 | else:
96 | subprocess.run(["git", "tag", new_version], check=True)
97 | subprocess.run(["git", "push"], check=True)
98 | subprocess.run(["git", "push", "origin", new_version], check=True)
99 |
100 |
101 | # メイン処理
102 | def main() -> None:
103 | parser = argparse.ArgumentParser(
104 | description="Update package.json version, run pnpm install, build, and commit changes."
105 | )
106 | parser.add_argument(
107 | "--dry-run",
108 | action="store_true",
109 | help="Run in dry-run mode without making actual changes",
110 | )
111 | args = parser.parse_args()
112 |
113 | package_json_path: str = "package.json"
114 |
115 | # バージョン更新
116 | new_version: Optional[str] = update_version(package_json_path, args.dry_run)
117 |
118 | if not new_version:
119 | return # ユーザーが確認をキャンセルした場合、処理を中断
120 |
121 | # バージョン更新後にまず git commit
122 | git_commit_version(new_version, args.dry_run)
123 |
124 | # pnpm install & build 実行
125 | run_pnpm_operations(args.dry_run)
126 |
127 | # ビルド後のファイルを git commit, タグ付け、プッシュ
128 | git_operations_after_build(new_version, args.dry_run)
129 |
130 |
131 | if __name__ == "__main__":
132 | main()
133 |
--------------------------------------------------------------------------------
/devtools/src/store/createSettingsSlice.ts:
--------------------------------------------------------------------------------
1 | import type { Direction } from '@open-ayame/ayame-web-sdk'
2 | import type { StateCreator } from 'zustand'
3 |
4 | type Settings = {
5 | audio: {
6 | isEnable: boolean
7 | direction: Direction
8 | codecMimeType: string
9 | }
10 | video: {
11 | isEnable: boolean
12 | direction: Direction
13 | codecMimeType: string
14 | resolution: string
15 | }
16 |
17 | signalingUrl: string
18 | roomId: string
19 | clientId: string
20 | signalingKey: string
21 |
22 | debug: boolean
23 | standalone: boolean
24 | }
25 |
26 | export interface SettingsSlice {
27 | settings: Settings
28 |
29 | toggleAudio: (enabled: boolean) => void
30 | setAudioDirection: (direction: Direction) => void
31 | setAudioCodecMimeType: (mimeType: string) => void
32 |
33 | toggleVideo: (enabled: boolean) => void
34 | setVideoDirection: (direction: Direction) => void
35 | setVideoCodecMimeType: (mimeType: string) => void
36 | setVideoResolution: (resolution: string) => void
37 |
38 | setSignalingUrl: (url: string) => void
39 | setRoomId: (roomId: string) => void
40 | setClientId: (clientId: string) => void
41 | setSignalingKey: (signalingKey: string) => void
42 | toggleDebug: (enabled: boolean) => void
43 | toggleStandalone: (enabled: boolean) => void
44 |
45 | // Copy URL 関連
46 | generateUrlParams: () => string
47 | setSettingsFromUrl: (params: URLSearchParams) => void
48 | }
49 |
50 | export const createSettingsSlice: StateCreator = (set, get) => ({
51 | // 初期値
52 | settings: {
53 | permissionState: {
54 | microphone: 'undefined',
55 | camera: 'undefined',
56 | },
57 | audio: {
58 | isEnable: true,
59 | direction: 'sendrecv',
60 | codecMimeType: 'undefined',
61 | },
62 | video: {
63 | isEnable: true,
64 | direction: 'sendrecv',
65 | codecMimeType: 'undefined',
66 | resolution: '640x480',
67 | },
68 | signalingUrl: import.meta.env.VITE_AYAME_SIGNALING_URL || '',
69 | roomId:
70 | `${import.meta.env.VITE_AYAME_ROOM_ID_PREFIX ?? ''}${import.meta.env.VITE_AYAME_ROOM_NAME ?? ''}` ||
71 | '',
72 | clientId: crypto.randomUUID(),
73 | signalingKey: import.meta.env.VITE_AYAME_SIGNALING_KEY || '',
74 | debug: false,
75 | standalone: false,
76 | },
77 |
78 | toggleAudio: (enabled: boolean) =>
79 | set((state) => ({
80 | settings: {
81 | ...state.settings,
82 | audio: {
83 | ...state.settings.audio,
84 | isEnable: enabled,
85 | },
86 | },
87 | })),
88 |
89 | setAudioDirection: (direction: Direction) =>
90 | set((state) => ({
91 | settings: {
92 | ...state.settings,
93 | audio: {
94 | ...state.settings.audio,
95 | direction: direction,
96 | },
97 | },
98 | })),
99 |
100 | setAudioCodecMimeType: (mimeType: string) =>
101 | set((state) => ({
102 | settings: {
103 | ...state.settings,
104 | audio: {
105 | ...state.settings.audio,
106 | codecMimeType: mimeType,
107 | },
108 | },
109 | })),
110 |
111 | toggleVideo: (enabled: boolean) =>
112 | set((state) => ({
113 | settings: {
114 | ...state.settings,
115 | video: {
116 | ...state.settings.video,
117 | isEnable: enabled,
118 | },
119 | },
120 | })),
121 |
122 | setVideoDirection: (direction: Direction) =>
123 | set((state) => ({
124 | settings: {
125 | ...state.settings,
126 | video: {
127 | ...state.settings.video,
128 | direction: direction,
129 | },
130 | },
131 | })),
132 |
133 | setVideoCodecMimeType: (mimeType: string) =>
134 | set((state) => ({
135 | settings: {
136 | ...state.settings,
137 | video: {
138 | ...state.settings.video,
139 | codecMimeType: mimeType,
140 | },
141 | },
142 | })),
143 |
144 | setVideoResolution: (resolution: string) =>
145 | set((state) => ({
146 | settings: {
147 | ...state.settings,
148 | video: {
149 | ...state.settings.video,
150 | resolution: resolution,
151 | },
152 | },
153 | })),
154 |
155 | setSignalingUrl: (url: string) => {
156 | set((state) => ({
157 | settings: {
158 | ...state.settings,
159 | signalingUrl: url,
160 | },
161 | }))
162 | },
163 |
164 | setRoomId: (roomId: string) => {
165 | set((state) => ({
166 | settings: {
167 | ...state.settings,
168 | roomId: roomId,
169 | },
170 | }))
171 | },
172 |
173 | setClientId: (clientId: string) => {
174 | set((state) => ({
175 | settings: {
176 | ...state.settings,
177 | clientId: clientId,
178 | },
179 | }))
180 | },
181 |
182 | setSignalingKey: (signalingKey: string) => {
183 | set((state) => ({
184 | settings: {
185 | ...state.settings,
186 | signalingKey: signalingKey,
187 | },
188 | }))
189 | },
190 |
191 | toggleDebug: (enabled: boolean) =>
192 | set((state) => ({
193 | settings: {
194 | ...state.settings,
195 | debug: enabled,
196 | },
197 | })),
198 |
199 | toggleStandalone: (enabled: boolean) =>
200 | set((state) => ({
201 | settings: {
202 | ...state.settings,
203 | standalone: enabled,
204 | },
205 | })),
206 |
207 | generateUrlParams: () => {
208 | const { settings } = get()
209 | const params = new URLSearchParams()
210 |
211 | params.set('audio', settings.audio.isEnable.toString())
212 | params.set('audioDirection', settings.audio.direction)
213 | params.set('audioCodecMimeType', settings.audio.codecMimeType)
214 |
215 | params.set('video', settings.video.isEnable.toString())
216 | params.set('videoDirection', settings.video.direction)
217 | params.set('videoCodecMimeType', settings.video.codecMimeType)
218 | params.set('videoResolution', settings.video.resolution)
219 |
220 | params.set('signalingUrl', settings.signalingUrl)
221 | params.set('roomId', settings.roomId)
222 | params.set('signalingKey', settings.signalingKey)
223 | params.set('debug', settings.debug.toString())
224 | params.set('standalone', settings.standalone.toString())
225 |
226 | return params.toString()
227 | },
228 |
229 | setSettingsFromUrl: (params: URLSearchParams) => {
230 | set((state) => ({
231 | settings: {
232 | ...state.settings,
233 | audio: {
234 | ...state.settings.audio,
235 | isEnable: params.get('audio') !== 'false',
236 | direction: (params.get('audioDirection') as Direction) || 'sendrecv',
237 | codecMimeType: params.get('audioCodecMimeType') || 'undefined',
238 | },
239 | video: {
240 | ...state.settings.video,
241 | isEnable: params.get('video') !== 'false',
242 | direction: (params.get('videoDirection') as Direction) || 'sendrecv',
243 | codecMimeType: params.get('videoCodecMimeType') || 'undefined',
244 | resolution: params.get('videoResolution') || '640x480',
245 | },
246 | // 項目がなかった場合は今ある値をそのまま利用する
247 | signalingUrl: params.get('signalingUrl') || state.settings.signalingUrl,
248 | roomId: params.get('roomId') || state.settings.roomId,
249 | clientId: params.get('clientId') || state.settings.clientId,
250 | signalingKey: params.get('signalingKey') || state.settings.signalingKey,
251 | debug: params.get('debug') === 'true',
252 | standalone: params.get('standalone') === 'true',
253 | },
254 | }))
255 | },
256 | })
257 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation, and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/src/ayame.ts:
--------------------------------------------------------------------------------
1 | import { version as ayameWebSdkVersion } from '../package.json'
2 | import type { ConnectionOptions, Direction, MetadataOption } from './types'
3 | import { getSelectedCodecs, traceLog } from './utils'
4 |
5 | interface AyameRegisterMessage {
6 | type: string
7 | roomId: string
8 | clientId: string
9 | key?: string
10 | authnMetadata?: any
11 | standalone?: boolean
12 | }
13 |
14 | export interface AyameAddStreamEvent {
15 | type: string
16 | stream: MediaStream
17 | }
18 |
19 | class Connection {
20 | private debug: boolean
21 | private roomId: string
22 | private signalingUrl: string
23 | private options: ConnectionOptions
24 | private connectionState: string
25 | private stream: MediaStream | null
26 | private remoteStream: MediaStream | null
27 | private authnMetadata: any
28 | private authzMetadata: any
29 | private ws: WebSocket | null
30 | private pc: RTCPeerConnection | null
31 | private callbacks: any
32 | private isOffer: boolean
33 | private isExistUser: boolean
34 | private dataChannels: RTCDataChannel[]
35 | private pcConfig: {
36 | iceServers: RTCIceServer[]
37 | iceTransportPolicy: RTCIceTransportPolicy
38 | }
39 |
40 | get webSocket(): WebSocket | null {
41 | return this.ws
42 | }
43 |
44 | get peerConnection(): RTCPeerConnection | null {
45 | return this.pc
46 | }
47 |
48 | // biome-ignore lint/complexity/noBannedTypes: Function type is needed for event callbacks
49 | on(kind: string, callback: Function): void {
50 | if (kind in this.callbacks) {
51 | this.callbacks[kind] = callback
52 | }
53 | }
54 |
55 | /**
56 | * オブジェクトを生成し、リモートのピアまたはサーバーに接続します。
57 | */
58 | constructor(
59 | signalingUrl: string,
60 | roomId: string,
61 | options: ConnectionOptions,
62 | debug = false,
63 | isRelay = false,
64 | ) {
65 | this.debug = debug
66 | this.roomId = roomId
67 | this.signalingUrl = signalingUrl
68 | this.options = options
69 | this.stream = null
70 | this.remoteStream = null
71 | this.pc = null
72 | this.ws = null
73 | this.authnMetadata = null
74 | this.authzMetadata = null
75 | this.dataChannels = []
76 | this.isOffer = false
77 | this.isExistUser = false
78 | this.connectionState = 'new'
79 | this.pcConfig = {
80 | iceServers: this.options.iceServers,
81 | iceTransportPolicy: isRelay ? 'relay' : 'all',
82 | }
83 | this.callbacks = {
84 | open: () => {},
85 | connect: () => {},
86 | disconnect: () => {},
87 | addstream: () => {},
88 | bye: () => {},
89 | datachannel: () => {},
90 | }
91 | }
92 |
93 | /**
94 | * 接続する
95 | */
96 | public async connect(
97 | stream: MediaStream | null,
98 | metadataOption: MetadataOption | null = null,
99 | ): Promise {
100 | if (this.ws) {
101 | this.traceLog('WebSocket Already Exists!')
102 | throw new Error('WebSocket Already Exists!')
103 | }
104 |
105 | if (this.pc) {
106 | this.traceLog('RTCPeerConnection already exists')
107 | throw new Error('RTCPeerConnection Already Exists!')
108 | }
109 |
110 | this.stream = stream
111 | if (metadataOption) {
112 | this.authnMetadata = metadataOption.authnMetadata
113 | }
114 | await this.signaling()
115 | }
116 |
117 | /**
118 | * 接続を切断する
119 | */
120 | public async disconnect(): Promise {
121 | // DataChannel を閉じる
122 | for (const dataChannel of this.dataChannels) {
123 | await this.closeDataChannel(dataChannel)
124 | }
125 | // WebSocket と PeerConnection を閉じる
126 | await Promise.all([this.closePeerConnection(), this.closeWebSocketConnection()])
127 |
128 | // 状態の初期化
129 | this.authzMetadata = null
130 | this.isOffer = false
131 | this.isExistUser = false
132 | this.dataChannels = []
133 | this.connectionState = 'new'
134 | }
135 |
136 | /**
137 | * 統計情報を取得する
138 | */
139 | public async getStats(): Promise {
140 | if (!this.pc) {
141 | throw new Error('PeerConnection is not ready')
142 | }
143 | return await this.pc.getStats()
144 | }
145 |
146 | private async signaling(): Promise {
147 | return new Promise((resolve, reject) => {
148 | if (this.ws) {
149 | return reject('WS-ALREADY-EXISTS')
150 | }
151 | this.ws = new WebSocket(this.signalingUrl)
152 | this.ws.onclose = async () => {
153 | if (!this.options.standalone) {
154 | await this.disconnect()
155 | this.callbacks.disconnect({ reason: 'WS-CLOSED' })
156 | return reject('WS-CLOSED')
157 | }
158 | }
159 | this.ws.onerror = async () => {
160 | await this.disconnect()
161 | return reject('WS-CLOSED-WITH-ERROR')
162 | }
163 | this.ws.onopen = () => {
164 | const registerMessage: AyameRegisterMessage = {
165 | type: 'register',
166 | roomId: this.roomId,
167 | clientId: this.options.clientId,
168 | authnMetadata: undefined,
169 | key: undefined,
170 | standalone: this.options.standalone,
171 | }
172 | if (this.authnMetadata !== null) {
173 | registerMessage.authnMetadata = this.authnMetadata
174 | }
175 | if (this.options.signalingKey !== null) {
176 | registerMessage.key = this.options.signalingKey
177 | }
178 | this.sendWs(registerMessage)
179 | if (this.ws) {
180 | this.ws.onmessage = async (event: MessageEvent) => {
181 | try {
182 | if (typeof event.data !== 'string') {
183 | return
184 | }
185 | const message = JSON.parse(event.data)
186 | if (message.type === 'ping') {
187 | this.sendWs({ type: 'pong' })
188 | } else if (message.type === 'bye') {
189 | this.callbacks.bye(event)
190 | return resolve()
191 | } else if (message.type === 'accept') {
192 | this.authzMetadata = message.authzMetadata
193 | if (Array.isArray(message.iceServers) && message.iceServers.length > 0) {
194 | this.traceLog('iceServers=>', message.iceServers)
195 | this.pcConfig.iceServers = message.iceServers
196 | }
197 | this.traceLog('isExistUser=>', message.isExistUser)
198 | this.isExistUser = message.isExistUser
199 | this.createPeerConnection()
200 | if (this.isExistUser === true) {
201 | await this.sendOffer()
202 | }
203 | return resolve()
204 | } else if (message.type === 'reject') {
205 | await this.disconnect()
206 | this.callbacks.disconnect({ reason: message.reason || 'REJECTED' })
207 | return reject('REJECTED')
208 | } else if (message.type === 'offer') {
209 | if (this.pc && this.pc.signalingState === 'have-local-offer') {
210 | this.createPeerConnection()
211 | }
212 | this.setOffer(new RTCSessionDescription(message))
213 | } else if (message.type === 'answer') {
214 | await this.setAnswer(new RTCSessionDescription(message))
215 | } else if (message.type === 'candidate') {
216 | if (message.ice) {
217 | this.traceLog('Received ICE candidate ...', message.ice)
218 | const candidate = new RTCIceCandidate(message.ice)
219 | this.addIceCandidate(candidate)
220 | }
221 | }
222 | } catch (error) {
223 | await this.disconnect()
224 | this.callbacks.disconnect({ reason: 'SIGNALING-ERROR', error: error })
225 | }
226 | }
227 | }
228 | }
229 | })
230 | }
231 |
232 | public async removeDataChannel(label: string): Promise {
233 | const dataChannel = this.findDataChannel(label)
234 | if (dataChannel && dataChannel.readyState === 'open') {
235 | await this.closeDataChannel(dataChannel)
236 | } else {
237 | throw new Error('data channel is not exist or open')
238 | }
239 | }
240 |
241 | private setCodecPreferences(
242 | kind: 'audio' | 'video',
243 | codecMimeType: string,
244 | capabilities: RTCRtpCapabilities,
245 | transceiver: RTCRtpTransceiver,
246 | ): void {
247 | this.traceLog(`${kind} codecMimeType=`, codecMimeType)
248 | // 指定されたコーデックが存在しない場合は return で返す
249 | if (!capabilities.codecs.some((codec) => codec.mimeType === codecMimeType)) {
250 | return
251 | }
252 |
253 | // codecPreferences が設定できない場合は return で返す
254 | if (typeof transceiver.setCodecPreferences !== 'function') {
255 | return
256 | }
257 |
258 | let codecs: RTCRtpCodecCapability[] = []
259 | codecs = getSelectedCodecs(kind, codecMimeType, capabilities.codecs)
260 | this.traceLog(`${kind} codecs=`, codecs)
261 | transceiver.setCodecPreferences(codecs)
262 | }
263 |
264 | private createPeerConnection(): void {
265 | this.traceLog('RTCConfiguration=>', this.pcConfig)
266 |
267 | const pc = new RTCPeerConnection(this.pcConfig)
268 |
269 | // sendrecv / sendonly が指定されている場合は setCodecPreferences を試みる
270 | if (this.stream && this.options.audio.direction !== 'recvonly') {
271 | // そもそも audioTracks が 0 じゃないかどうか確認する
272 | const audioTracks = this.stream.getAudioTracks()
273 | if (audioTracks.length > 0) {
274 | const audioTrack = audioTracks[0]
275 | const audioSender = pc.addTrack(audioTrack, this.stream)
276 | const audioTransceiver = this.getTransceiver(pc, audioSender)
277 | if (audioTransceiver) {
278 | audioTransceiver.direction = this.options.audio.direction
279 | }
280 | const audioCapabilities = RTCRtpSender.getCapabilities('audio')
281 | // コーデックが指定されていた場合は setCodecPreferences を試みる
282 | if (
283 | this.options.audio.enabled &&
284 | this.options.audio.codecMimeType !== undefined &&
285 | audioTransceiver !== null &&
286 | audioCapabilities !== null
287 | ) {
288 | this.setCodecPreferences(
289 | 'audio',
290 | this.options.audio.codecMimeType,
291 | audioCapabilities,
292 | audioTransceiver,
293 | )
294 | }
295 | }
296 | // 基本的に受信側はコーデック指定はしないほうがいい
297 | // recvonly で audio が有効な場合、
298 | } else if (this.options.audio.enabled) {
299 | const audioTransceiver = pc.addTransceiver('audio', {
300 | direction: this.options.audio.direction,
301 | })
302 | const audioCapabilities = RTCRtpReceiver.getCapabilities('audio')
303 | // コーデックが指定されていた場合は setCodecPreferences を試みる
304 | if (
305 | this.options.audio.enabled &&
306 | this.options.audio.codecMimeType !== undefined &&
307 | audioCapabilities !== null
308 | ) {
309 | // コーデックを指定された場合は受信出来るかどうかの確認をする
310 | this.setCodecPreferences(
311 | 'audio',
312 | this.options.audio.codecMimeType,
313 | audioCapabilities,
314 | audioTransceiver,
315 | )
316 | }
317 | }
318 |
319 | // sendrecv / sendonly が指定されている場合は setCodecPreferences を試みる
320 | if (this.stream && this.options.video.direction !== 'recvonly') {
321 | // そもそも videoTracks が 0 じゃないかどうか確認する
322 | const videoTracks = this.stream.getVideoTracks()
323 | if (videoTracks.length > 0) {
324 | const videoTrack = videoTracks[0]
325 | const videoSender = pc.addTrack(videoTrack, this.stream)
326 | const videoTransceiver = this.getTransceiver(pc, videoSender)
327 | if (videoTransceiver) {
328 | videoTransceiver.direction = this.options.video.direction
329 | }
330 | const videoCapabilities = RTCRtpSender.getCapabilities('video')
331 | // コーデックが指定されていた場合は setCodecPreferences を試みる
332 | if (
333 | this.options.video.enabled &&
334 | this.options.video.codecMimeType !== undefined &&
335 | videoTransceiver !== null &&
336 | videoCapabilities !== null
337 | ) {
338 | this.setCodecPreferences(
339 | 'video',
340 | this.options.video.codecMimeType,
341 | videoCapabilities,
342 | videoTransceiver,
343 | )
344 | }
345 | }
346 | // 基本的に受信側はコーデック指定はしないほうがいい
347 | // recvonly で video が有効な場合、
348 | } else if (this.options.video.enabled) {
349 | const videoTransceiver = pc.addTransceiver('video', {
350 | direction: this.options.video.direction,
351 | })
352 | const videoCapabilities = RTCRtpReceiver.getCapabilities('video')
353 | // コーデックが指定されていた場合は setCodecPreferences を試みる
354 | if (
355 | this.options.video.enabled &&
356 | this.options.video.codecMimeType !== undefined &&
357 | videoTransceiver !== null &&
358 | videoCapabilities !== null
359 | ) {
360 | this.setCodecPreferences(
361 | 'video',
362 | this.options.video.codecMimeType,
363 | videoCapabilities,
364 | videoTransceiver,
365 | )
366 | }
367 | }
368 |
369 | const _tracks: MediaStreamTrack[] = []
370 | pc.ontrack = (event: RTCTrackEvent) => {
371 | // すでに remoteStream がある場合はなにもしない
372 | if (this.remoteStream) {
373 | return
374 | }
375 | this.traceLog('peer.ontrack()', event)
376 | this.remoteStream = event.streams[0]
377 | const callbackEvent: AyameAddStreamEvent = {
378 | type: 'addstream',
379 | stream: this.remoteStream,
380 | }
381 | this.callbacks.addstream(callbackEvent)
382 | }
383 | pc.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
384 | this.traceLog('peer.onicecandidate()', event)
385 | if (event.candidate) {
386 | this.sendIceCandidate(event.candidate)
387 | } else {
388 | this.traceLog('empty ice event', '')
389 | }
390 | }
391 | pc.oniceconnectionstatechange = async () => {
392 | this.traceLog('ICE connection Status has changed to ', pc.iceConnectionState)
393 | if (this.connectionState !== pc.iceConnectionState) {
394 | this.connectionState = pc.iceConnectionState
395 | switch (this.connectionState) {
396 | case 'connected':
397 | this.isOffer = false
398 | this.callbacks.connect()
399 | break
400 | case 'disconnected':
401 | case 'failed':
402 | await this.disconnect()
403 | this.callbacks.disconnect({ reason: 'ICE-CONNECTION-STATE-FAILED' })
404 | break
405 | }
406 | }
407 | }
408 | pc.onconnectionstatechange = async (_event: Event) => {
409 | if (pc.connectionState === 'connected') {
410 | if (this.options.standalone) {
411 | this.sendWs({ type: 'connected' })
412 | if (this.ws) {
413 | this.traceLog('websocket is closed')
414 | this.ws.close()
415 | this.ws = null
416 | }
417 | }
418 | } else if (pc.connectionState === 'closed') {
419 | this.traceLog('peer connection is closed')
420 | await this.disconnect()
421 | }
422 | }
423 | pc.onsignalingstatechange = (_) => {
424 | this.traceLog('signaling state changes:', pc.signalingState)
425 | }
426 | pc.ondatachannel = this.onDataChannel.bind(this)
427 | if (!this.pc) {
428 | this.pc = pc
429 | this.callbacks.open({ authzMetadata: this.authzMetadata })
430 | } else {
431 | this.pc = pc
432 | }
433 | }
434 |
435 | public async createDataChannel(
436 | label: string,
437 | options: RTCDataChannelInit | undefined,
438 | ): Promise {
439 | return new Promise((resolve, reject) => {
440 | if (!this.pc) return reject('PeerConnection Does Not Ready')
441 | if (this.isOffer) return reject('PeerConnection Has Local Offer')
442 | let dataChannel = this.findDataChannel(label)
443 | if (dataChannel) {
444 | return reject('DataChannel Already Exists!')
445 | }
446 | if (this.isExistUser) {
447 | dataChannel = this.pc.createDataChannel(label, options)
448 | dataChannel.onclose = (event: Record) => {
449 | this.traceLog('datachannel onclosed=>', event)
450 | this.dataChannels = this.dataChannels.filter((dataChannel) => dataChannel.label !== label)
451 | }
452 | dataChannel.onerror = (event: Record) => {
453 | this.traceLog('datachannel onerror=>', event)
454 | this.dataChannels = this.dataChannels.filter((dataChannel) => dataChannel.label !== label)
455 | }
456 | dataChannel.onmessage = (event: any) => {
457 | this.traceLog('datachannel onmessage=>', event.data)
458 | event.label = label
459 | }
460 | dataChannel.onopen = (event: Record) => {
461 | this.traceLog('datachannel onopen=>', event)
462 | }
463 | this.dataChannels.push(dataChannel)
464 | return resolve(dataChannel)
465 | }
466 | return resolve(null)
467 | })
468 | }
469 |
470 | private onDataChannel(event: RTCDataChannelEvent): void {
471 | this.traceLog('on data channel', event)
472 | if (!this.pc) return
473 | const dataChannel = event.channel
474 | const label = event.channel.label
475 | if (!event.channel) return
476 | if (!label || label.length < 1) return
477 | dataChannel.onopen = async (event: Record) => {
478 | this.traceLog('datachannel onopen=>', event)
479 | }
480 | dataChannel.onclose = async (event: Record) => {
481 | this.traceLog('datachannel onclosed=>', event)
482 | }
483 | dataChannel.onerror = async (event: Record) => {
484 | this.traceLog('datachannel onerror=>', event)
485 | }
486 | dataChannel.onmessage = (event: any) => {
487 | this.traceLog('datachannel onmessage=>', event.data)
488 | event.label = label
489 | }
490 | if (!this.findDataChannel(label)) {
491 | this.dataChannels.push(event.channel)
492 | } else {
493 | this.dataChannels = this.dataChannels.map((channel) => {
494 | if (channel.label === label) {
495 | return dataChannel
496 | }
497 | return channel
498 | })
499 | }
500 | this.callbacks.datachannel(dataChannel)
501 | }
502 |
503 | private async sendOffer(): Promise {
504 | if (!this.pc) {
505 | return
506 | }
507 |
508 | const offer: any = await this.pc.createOffer({
509 | offerToReceiveAudio:
510 | this.options.audio.enabled && this.options.audio.direction !== 'sendonly',
511 | offerToReceiveVideo:
512 | this.options.video.enabled && this.options.video.direction !== 'sendonly',
513 | })
514 | this.traceLog('create offer sdp, sdp=', offer.sdp)
515 | await this.pc.setLocalDescription(offer)
516 | if (this.pc.localDescription) {
517 | this.sendSdp(this.pc.localDescription)
518 | }
519 | this.isOffer = true
520 | }
521 |
522 | private isAudioCodecSpecified(): boolean {
523 | return this.options.audio.enabled && this.options.audio.codecMimeType !== undefined
524 | }
525 |
526 | private isVideoCodecSpecified(): boolean {
527 | return this.options.video.enabled && this.options.video.codecMimeType !== undefined
528 | }
529 |
530 | private async createAnswer(): Promise {
531 | if (!this.pc) {
532 | return
533 | }
534 | try {
535 | const answer = await this.pc.createAnswer()
536 | this.traceLog('create answer sdp, sdp=', answer.sdp)
537 | await this.pc.setLocalDescription(answer)
538 | if (this.pc.localDescription) this.sendSdp(this.pc.localDescription)
539 | } catch (error) {
540 | await this.disconnect()
541 | this.callbacks.disconnect({ reason: 'CREATE-ANSWER-ERROR', error: error })
542 | }
543 | }
544 |
545 | private async setAnswer(sessionDescription: RTCSessionDescription): Promise {
546 | if (!this.pc) {
547 | return
548 | }
549 | await this.pc.setRemoteDescription(sessionDescription)
550 | this.traceLog('set answer sdp=', sessionDescription.sdp)
551 | }
552 |
553 | private async setOffer(sessionDescription: RTCSessionDescription): Promise {
554 | try {
555 | if (!this.pc) {
556 | return
557 | }
558 | await this.pc.setRemoteDescription(sessionDescription)
559 | this.traceLog('set offer sdp=', sessionDescription.sdp)
560 | await this.createAnswer()
561 | } catch (error) {
562 | await this.disconnect()
563 | this.callbacks.disconnect({ reason: 'SET-OFFER-ERROR', error: error })
564 | }
565 | }
566 |
567 | private async addIceCandidate(candidate: RTCIceCandidate): Promise {
568 | try {
569 | if (this.pc) {
570 | await this.pc.addIceCandidate(candidate)
571 | }
572 | } catch (_error) {
573 | this.traceLog('invalid ice candidate', candidate)
574 | }
575 | }
576 |
577 | private sendIceCandidate(candidate: RTCIceCandidate): void {
578 | const message = { type: 'candidate', ice: candidate }
579 | this.sendWs(message)
580 | }
581 |
582 | private sendSdp(sessionDescription: RTCSessionDescription): void {
583 | this.sendWs(sessionDescription)
584 | }
585 |
586 | private sendWs(message: Record) {
587 | if (this.ws) {
588 | this.ws.send(JSON.stringify(message))
589 | }
590 | }
591 |
592 | private getTransceiver(pc: RTCPeerConnection, track: any): RTCRtpTransceiver | null {
593 | let transceiver = null
594 | for (const t of pc.getTransceivers()) {
595 | if (t.sender === track || t.receiver === track) {
596 | transceiver = t
597 | }
598 | }
599 | if (!transceiver) {
600 | throw new Error('invalid transceiver')
601 | }
602 | return transceiver
603 | }
604 |
605 | private findDataChannel(label: string): RTCDataChannel | undefined {
606 | return this.dataChannels.find((channel) => channel.label === label)
607 | }
608 |
609 | private async closeDataChannel(dataChannel: RTCDataChannel): Promise {
610 | this.traceLog('close data channel')
611 | return new Promise((resolve) => {
612 | if (!dataChannel) {
613 | this.traceLog('data channel is null')
614 | return resolve()
615 | }
616 | if (dataChannel.readyState === 'closed') {
617 | this.traceLog('data channel is closed')
618 | return resolve()
619 | }
620 | dataChannel.onclose = null
621 | const timerId = setInterval(() => {
622 | if (dataChannel.readyState === 'closed') {
623 | clearInterval(timerId)
624 | this.traceLog('data channel is closed')
625 | return resolve()
626 | }
627 | }, 200)
628 | dataChannel.close()
629 | })
630 | }
631 |
632 | private async closePeerConnection(): Promise {
633 | this.traceLog('close peer connection')
634 | return new Promise((resolve) => {
635 | if (!this.pc) {
636 | this.traceLog('peer connection is null')
637 | return resolve()
638 | }
639 | if (this.pc.connectionState === 'closed') {
640 | this.pc = null
641 | this.traceLog('peer connection is closed')
642 | return resolve()
643 | }
644 | this.pc.oniceconnectionstatechange = null
645 | const timerId = setInterval(() => {
646 | if (!this.pc) {
647 | clearInterval(timerId)
648 | this.traceLog('peer connection is null')
649 | return resolve()
650 | }
651 | if (this.pc.connectionState === 'closed') {
652 | this.pc = null
653 | clearInterval(timerId)
654 | this.traceLog('peer connection is closed')
655 | return resolve()
656 | }
657 | }, 200)
658 | this.pc.close()
659 | })
660 | }
661 |
662 | private async closeWebSocketConnection(): Promise {
663 | return new Promise((resolve) => {
664 | // WS がない場合はすでに閉じられているので resolve
665 | if (!this.ws) {
666 | this.traceLog('websocket is null')
667 | return resolve()
668 | }
669 | // WS がすでに閉じられている場合は resolve
670 | if (this.ws && this.ws.readyState === WebSocket.CLOSED) {
671 | this.ws = null
672 | this.traceLog('websocket is closed')
673 | return resolve()
674 | }
675 | // WS の onclose を null 入れる
676 | this.ws.onclose = null
677 | // WS が閉じられるまで待つ
678 | const timerId = setInterval(() => {
679 | // WS がない場合はすでに閉じられているので resolve
680 | if (!this.ws) {
681 | clearInterval(timerId)
682 | this.traceLog('websocket is null')
683 | return resolve()
684 | }
685 | // WS が閉じられている場合は resolve
686 | if (this.ws.readyState === WebSocket.CLOSED) {
687 | this.ws = null
688 | clearInterval(timerId)
689 | this.traceLog('websocket is closed')
690 | return resolve()
691 | }
692 | }, 200)
693 | // WS を閉じる
694 | this.ws.close()
695 | })
696 | }
697 |
698 | private traceLog(title: string, message?: Record | string) {
699 | if (!this.debug) {
700 | return
701 | }
702 | traceLog(title, message)
703 | }
704 | }
705 |
706 | export default Connection
707 |
708 | /**
709 | * Ayame Connection のデフォルトのオプションです。
710 | */
711 | export const defaultOptions: ConnectionOptions = {
712 | audio: { direction: 'sendrecv', enabled: true },
713 | video: { direction: 'sendrecv', enabled: true },
714 | iceServers: [],
715 | clientId: crypto.randomUUID(),
716 | }
717 |
718 | /**
719 | * Ayame Connection を生成します。
720 | * @deprecated この関数は廃止予定です。代わりに createConnection を使用してください。
721 | */
722 | export const connection = (
723 | signalingUrl: string,
724 | roomId: string,
725 | options: ConnectionOptions = defaultOptions,
726 | debug = false,
727 | isRelay = false,
728 | ): Connection => {
729 | return new Connection(signalingUrl, roomId, options, debug, isRelay)
730 | }
731 |
732 | /**
733 | * Ayame Connection を生成します。
734 | */
735 | export const createConnection = (
736 | signalingUrl: string,
737 | roomId: string,
738 | options: ConnectionOptions = defaultOptions,
739 | debug = false,
740 | isRelay = false,
741 | ): Connection => {
742 | return new Connection(signalingUrl, roomId, options, debug, isRelay)
743 | }
744 |
745 | /**
746 | * Ayame Web SDK のバージョンを出力します。
747 | */
748 | export const version = (): string => {
749 | return ayameWebSdkVersion
750 | }
751 |
752 | export type { Connection, ConnectionOptions, Direction, MetadataOption }
753 | export { getAvailableCodecs } from './utils'
754 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | devDependencies:
11 | '@biomejs/biome':
12 | specifier: 2.3.10
13 | version: 2.3.10
14 | '@playwright/test':
15 | specifier: 1.57.0
16 | version: 1.57.0
17 | '@types/node':
18 | specifier: 24.9.1
19 | version: 24.9.1
20 | '@types/webrtc':
21 | specifier: 0.0.47
22 | version: 0.0.47
23 | typedoc:
24 | specifier: 0.28.15
25 | version: 0.28.15(typescript@5.9.3)
26 | typescript:
27 | specifier: 5.9.3
28 | version: 5.9.3
29 | vite:
30 | specifier: 7.3.0
31 | version: 7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1)
32 | vite-plugin-dts:
33 | specifier: 4.5.4
34 | version: 4.5.4(@types/node@24.9.1)(rollup@4.46.4)(typescript@5.9.3)(vite@7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1))
35 |
36 | devtools:
37 | dependencies:
38 | '@open-ayame/ayame-web-sdk':
39 | specifier: workspace:*
40 | version: link:..
41 | react:
42 | specifier: 19.2.3
43 | version: 19.2.3
44 | react-dom:
45 | specifier: 19.2.3
46 | version: 19.2.3(react@19.2.3)
47 | zustand:
48 | specifier: 5.0.9
49 | version: 5.0.9(@types/react@19.2.7)(react@19.2.3)
50 | devDependencies:
51 | '@types/react':
52 | specifier: 19.2.7
53 | version: 19.2.7
54 | '@types/react-dom':
55 | specifier: 19.2.3
56 | version: 19.2.3(@types/react@19.2.7)
57 | '@vitejs/plugin-react':
58 | specifier: 5.1.2
59 | version: 5.1.2(vite@7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1))
60 |
61 | packages:
62 |
63 | '@babel/code-frame@7.27.1':
64 | resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
65 | engines: {node: '>=6.9.0'}
66 |
67 | '@babel/compat-data@7.28.0':
68 | resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
69 | engines: {node: '>=6.9.0'}
70 |
71 | '@babel/core@7.28.5':
72 | resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
73 | engines: {node: '>=6.9.0'}
74 |
75 | '@babel/generator@7.28.5':
76 | resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
77 | engines: {node: '>=6.9.0'}
78 |
79 | '@babel/helper-compilation-targets@7.27.2':
80 | resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
81 | engines: {node: '>=6.9.0'}
82 |
83 | '@babel/helper-globals@7.28.0':
84 | resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
85 | engines: {node: '>=6.9.0'}
86 |
87 | '@babel/helper-module-imports@7.27.1':
88 | resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
89 | engines: {node: '>=6.9.0'}
90 |
91 | '@babel/helper-module-transforms@7.28.3':
92 | resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
93 | engines: {node: '>=6.9.0'}
94 | peerDependencies:
95 | '@babel/core': ^7.0.0
96 |
97 | '@babel/helper-plugin-utils@7.27.1':
98 | resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
99 | engines: {node: '>=6.9.0'}
100 |
101 | '@babel/helper-string-parser@7.27.1':
102 | resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
103 | engines: {node: '>=6.9.0'}
104 |
105 | '@babel/helper-validator-identifier@7.28.5':
106 | resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
107 | engines: {node: '>=6.9.0'}
108 |
109 | '@babel/helper-validator-option@7.27.1':
110 | resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
111 | engines: {node: '>=6.9.0'}
112 |
113 | '@babel/helpers@7.28.4':
114 | resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
115 | engines: {node: '>=6.9.0'}
116 |
117 | '@babel/parser@7.28.5':
118 | resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
119 | engines: {node: '>=6.0.0'}
120 | hasBin: true
121 |
122 | '@babel/plugin-transform-react-jsx-self@7.27.1':
123 | resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
124 | engines: {node: '>=6.9.0'}
125 | peerDependencies:
126 | '@babel/core': ^7.0.0-0
127 |
128 | '@babel/plugin-transform-react-jsx-source@7.27.1':
129 | resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
130 | engines: {node: '>=6.9.0'}
131 | peerDependencies:
132 | '@babel/core': ^7.0.0-0
133 |
134 | '@babel/template@7.27.2':
135 | resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
136 | engines: {node: '>=6.9.0'}
137 |
138 | '@babel/traverse@7.28.5':
139 | resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
140 | engines: {node: '>=6.9.0'}
141 |
142 | '@babel/types@7.28.5':
143 | resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
144 | engines: {node: '>=6.9.0'}
145 |
146 | '@biomejs/biome@2.3.10':
147 | resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==}
148 | engines: {node: '>=14.21.3'}
149 | hasBin: true
150 |
151 | '@biomejs/cli-darwin-arm64@2.3.10':
152 | resolution: {integrity: sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==}
153 | engines: {node: '>=14.21.3'}
154 | cpu: [arm64]
155 | os: [darwin]
156 |
157 | '@biomejs/cli-darwin-x64@2.3.10':
158 | resolution: {integrity: sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==}
159 | engines: {node: '>=14.21.3'}
160 | cpu: [x64]
161 | os: [darwin]
162 |
163 | '@biomejs/cli-linux-arm64-musl@2.3.10':
164 | resolution: {integrity: sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==}
165 | engines: {node: '>=14.21.3'}
166 | cpu: [arm64]
167 | os: [linux]
168 |
169 | '@biomejs/cli-linux-arm64@2.3.10':
170 | resolution: {integrity: sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==}
171 | engines: {node: '>=14.21.3'}
172 | cpu: [arm64]
173 | os: [linux]
174 |
175 | '@biomejs/cli-linux-x64-musl@2.3.10':
176 | resolution: {integrity: sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==}
177 | engines: {node: '>=14.21.3'}
178 | cpu: [x64]
179 | os: [linux]
180 |
181 | '@biomejs/cli-linux-x64@2.3.10':
182 | resolution: {integrity: sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==}
183 | engines: {node: '>=14.21.3'}
184 | cpu: [x64]
185 | os: [linux]
186 |
187 | '@biomejs/cli-win32-arm64@2.3.10':
188 | resolution: {integrity: sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==}
189 | engines: {node: '>=14.21.3'}
190 | cpu: [arm64]
191 | os: [win32]
192 |
193 | '@biomejs/cli-win32-x64@2.3.10':
194 | resolution: {integrity: sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==}
195 | engines: {node: '>=14.21.3'}
196 | cpu: [x64]
197 | os: [win32]
198 |
199 | '@esbuild/aix-ppc64@0.27.1':
200 | resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==}
201 | engines: {node: '>=18'}
202 | cpu: [ppc64]
203 | os: [aix]
204 |
205 | '@esbuild/android-arm64@0.27.1':
206 | resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==}
207 | engines: {node: '>=18'}
208 | cpu: [arm64]
209 | os: [android]
210 |
211 | '@esbuild/android-arm@0.27.1':
212 | resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==}
213 | engines: {node: '>=18'}
214 | cpu: [arm]
215 | os: [android]
216 |
217 | '@esbuild/android-x64@0.27.1':
218 | resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==}
219 | engines: {node: '>=18'}
220 | cpu: [x64]
221 | os: [android]
222 |
223 | '@esbuild/darwin-arm64@0.27.1':
224 | resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==}
225 | engines: {node: '>=18'}
226 | cpu: [arm64]
227 | os: [darwin]
228 |
229 | '@esbuild/darwin-x64@0.27.1':
230 | resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==}
231 | engines: {node: '>=18'}
232 | cpu: [x64]
233 | os: [darwin]
234 |
235 | '@esbuild/freebsd-arm64@0.27.1':
236 | resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==}
237 | engines: {node: '>=18'}
238 | cpu: [arm64]
239 | os: [freebsd]
240 |
241 | '@esbuild/freebsd-x64@0.27.1':
242 | resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==}
243 | engines: {node: '>=18'}
244 | cpu: [x64]
245 | os: [freebsd]
246 |
247 | '@esbuild/linux-arm64@0.27.1':
248 | resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==}
249 | engines: {node: '>=18'}
250 | cpu: [arm64]
251 | os: [linux]
252 |
253 | '@esbuild/linux-arm@0.27.1':
254 | resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==}
255 | engines: {node: '>=18'}
256 | cpu: [arm]
257 | os: [linux]
258 |
259 | '@esbuild/linux-ia32@0.27.1':
260 | resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==}
261 | engines: {node: '>=18'}
262 | cpu: [ia32]
263 | os: [linux]
264 |
265 | '@esbuild/linux-loong64@0.27.1':
266 | resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==}
267 | engines: {node: '>=18'}
268 | cpu: [loong64]
269 | os: [linux]
270 |
271 | '@esbuild/linux-mips64el@0.27.1':
272 | resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==}
273 | engines: {node: '>=18'}
274 | cpu: [mips64el]
275 | os: [linux]
276 |
277 | '@esbuild/linux-ppc64@0.27.1':
278 | resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==}
279 | engines: {node: '>=18'}
280 | cpu: [ppc64]
281 | os: [linux]
282 |
283 | '@esbuild/linux-riscv64@0.27.1':
284 | resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==}
285 | engines: {node: '>=18'}
286 | cpu: [riscv64]
287 | os: [linux]
288 |
289 | '@esbuild/linux-s390x@0.27.1':
290 | resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==}
291 | engines: {node: '>=18'}
292 | cpu: [s390x]
293 | os: [linux]
294 |
295 | '@esbuild/linux-x64@0.27.1':
296 | resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==}
297 | engines: {node: '>=18'}
298 | cpu: [x64]
299 | os: [linux]
300 |
301 | '@esbuild/netbsd-arm64@0.27.1':
302 | resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==}
303 | engines: {node: '>=18'}
304 | cpu: [arm64]
305 | os: [netbsd]
306 |
307 | '@esbuild/netbsd-x64@0.27.1':
308 | resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==}
309 | engines: {node: '>=18'}
310 | cpu: [x64]
311 | os: [netbsd]
312 |
313 | '@esbuild/openbsd-arm64@0.27.1':
314 | resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==}
315 | engines: {node: '>=18'}
316 | cpu: [arm64]
317 | os: [openbsd]
318 |
319 | '@esbuild/openbsd-x64@0.27.1':
320 | resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==}
321 | engines: {node: '>=18'}
322 | cpu: [x64]
323 | os: [openbsd]
324 |
325 | '@esbuild/openharmony-arm64@0.27.1':
326 | resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==}
327 | engines: {node: '>=18'}
328 | cpu: [arm64]
329 | os: [openharmony]
330 |
331 | '@esbuild/sunos-x64@0.27.1':
332 | resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==}
333 | engines: {node: '>=18'}
334 | cpu: [x64]
335 | os: [sunos]
336 |
337 | '@esbuild/win32-arm64@0.27.1':
338 | resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==}
339 | engines: {node: '>=18'}
340 | cpu: [arm64]
341 | os: [win32]
342 |
343 | '@esbuild/win32-ia32@0.27.1':
344 | resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==}
345 | engines: {node: '>=18'}
346 | cpu: [ia32]
347 | os: [win32]
348 |
349 | '@esbuild/win32-x64@0.27.1':
350 | resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==}
351 | engines: {node: '>=18'}
352 | cpu: [x64]
353 | os: [win32]
354 |
355 | '@gerrit0/mini-shiki@3.17.0':
356 | resolution: {integrity: sha512-Bpf6WuFar20ZXL6qU6VpVl4bVQfyyYiX+6O4xrns4nkU3Mr8paeupDbS1HENpcLOYj7pN4Rkd/yCaPA0vQwKww==}
357 |
358 | '@isaacs/balanced-match@4.0.1':
359 | resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
360 | engines: {node: 20 || >=22}
361 |
362 | '@isaacs/brace-expansion@5.0.0':
363 | resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
364 | engines: {node: 20 || >=22}
365 |
366 | '@jridgewell/gen-mapping@0.3.13':
367 | resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
368 |
369 | '@jridgewell/remapping@2.3.5':
370 | resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
371 |
372 | '@jridgewell/resolve-uri@3.1.2':
373 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
374 | engines: {node: '>=6.0.0'}
375 |
376 | '@jridgewell/source-map@0.3.11':
377 | resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
378 |
379 | '@jridgewell/sourcemap-codec@1.5.5':
380 | resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
381 |
382 | '@jridgewell/trace-mapping@0.3.30':
383 | resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
384 |
385 | '@microsoft/api-extractor-model@7.30.7':
386 | resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==}
387 |
388 | '@microsoft/api-extractor@7.52.11':
389 | resolution: {integrity: sha512-IKQ7bHg6f/Io3dQds6r9QPYk4q0OlR9A4nFDtNhUt3UUIhyitbxAqRN1CLjUVtk6IBk3xzyCMOdwwtIXQ7AlGg==}
390 | hasBin: true
391 |
392 | '@microsoft/tsdoc-config@0.17.1':
393 | resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
394 |
395 | '@microsoft/tsdoc@0.15.1':
396 | resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
397 |
398 | '@parcel/watcher-android-arm64@2.5.1':
399 | resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
400 | engines: {node: '>= 10.0.0'}
401 | cpu: [arm64]
402 | os: [android]
403 |
404 | '@parcel/watcher-darwin-arm64@2.5.1':
405 | resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
406 | engines: {node: '>= 10.0.0'}
407 | cpu: [arm64]
408 | os: [darwin]
409 |
410 | '@parcel/watcher-darwin-x64@2.5.1':
411 | resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
412 | engines: {node: '>= 10.0.0'}
413 | cpu: [x64]
414 | os: [darwin]
415 |
416 | '@parcel/watcher-freebsd-x64@2.5.1':
417 | resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
418 | engines: {node: '>= 10.0.0'}
419 | cpu: [x64]
420 | os: [freebsd]
421 |
422 | '@parcel/watcher-linux-arm-glibc@2.5.1':
423 | resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
424 | engines: {node: '>= 10.0.0'}
425 | cpu: [arm]
426 | os: [linux]
427 |
428 | '@parcel/watcher-linux-arm-musl@2.5.1':
429 | resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
430 | engines: {node: '>= 10.0.0'}
431 | cpu: [arm]
432 | os: [linux]
433 |
434 | '@parcel/watcher-linux-arm64-glibc@2.5.1':
435 | resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
436 | engines: {node: '>= 10.0.0'}
437 | cpu: [arm64]
438 | os: [linux]
439 |
440 | '@parcel/watcher-linux-arm64-musl@2.5.1':
441 | resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
442 | engines: {node: '>= 10.0.0'}
443 | cpu: [arm64]
444 | os: [linux]
445 |
446 | '@parcel/watcher-linux-x64-glibc@2.5.1':
447 | resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
448 | engines: {node: '>= 10.0.0'}
449 | cpu: [x64]
450 | os: [linux]
451 |
452 | '@parcel/watcher-linux-x64-musl@2.5.1':
453 | resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
454 | engines: {node: '>= 10.0.0'}
455 | cpu: [x64]
456 | os: [linux]
457 |
458 | '@parcel/watcher-win32-arm64@2.5.1':
459 | resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
460 | engines: {node: '>= 10.0.0'}
461 | cpu: [arm64]
462 | os: [win32]
463 |
464 | '@parcel/watcher-win32-ia32@2.5.1':
465 | resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
466 | engines: {node: '>= 10.0.0'}
467 | cpu: [ia32]
468 | os: [win32]
469 |
470 | '@parcel/watcher-win32-x64@2.5.1':
471 | resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
472 | engines: {node: '>= 10.0.0'}
473 | cpu: [x64]
474 | os: [win32]
475 |
476 | '@parcel/watcher@2.5.1':
477 | resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
478 | engines: {node: '>= 10.0.0'}
479 |
480 | '@playwright/test@1.57.0':
481 | resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==}
482 | engines: {node: '>=18'}
483 | hasBin: true
484 |
485 | '@rolldown/pluginutils@1.0.0-beta.53':
486 | resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
487 |
488 | '@rollup/pluginutils@5.2.0':
489 | resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
490 | engines: {node: '>=14.0.0'}
491 | peerDependencies:
492 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
493 | peerDependenciesMeta:
494 | rollup:
495 | optional: true
496 |
497 | '@rollup/rollup-android-arm-eabi@4.46.4':
498 | resolution: {integrity: sha512-B2wfzCJ+ps/OBzRjeds7DlJumCU3rXMxJJS1vzURyj7+KBHGONm7c9q1TfdBl4vCuNMkDvARn3PBl2wZzuR5mw==}
499 | cpu: [arm]
500 | os: [android]
501 |
502 | '@rollup/rollup-android-arm64@4.46.4':
503 | resolution: {integrity: sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==}
504 | cpu: [arm64]
505 | os: [android]
506 |
507 | '@rollup/rollup-darwin-arm64@4.46.4':
508 | resolution: {integrity: sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==}
509 | cpu: [arm64]
510 | os: [darwin]
511 |
512 | '@rollup/rollup-darwin-x64@4.46.4':
513 | resolution: {integrity: sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==}
514 | cpu: [x64]
515 | os: [darwin]
516 |
517 | '@rollup/rollup-freebsd-arm64@4.46.4':
518 | resolution: {integrity: sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==}
519 | cpu: [arm64]
520 | os: [freebsd]
521 |
522 | '@rollup/rollup-freebsd-x64@4.46.4':
523 | resolution: {integrity: sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==}
524 | cpu: [x64]
525 | os: [freebsd]
526 |
527 | '@rollup/rollup-linux-arm-gnueabihf@4.46.4':
528 | resolution: {integrity: sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==}
529 | cpu: [arm]
530 | os: [linux]
531 |
532 | '@rollup/rollup-linux-arm-musleabihf@4.46.4':
533 | resolution: {integrity: sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==}
534 | cpu: [arm]
535 | os: [linux]
536 |
537 | '@rollup/rollup-linux-arm64-gnu@4.46.4':
538 | resolution: {integrity: sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==}
539 | cpu: [arm64]
540 | os: [linux]
541 |
542 | '@rollup/rollup-linux-arm64-musl@4.46.4':
543 | resolution: {integrity: sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==}
544 | cpu: [arm64]
545 | os: [linux]
546 |
547 | '@rollup/rollup-linux-loongarch64-gnu@4.46.4':
548 | resolution: {integrity: sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==}
549 | cpu: [loong64]
550 | os: [linux]
551 |
552 | '@rollup/rollup-linux-ppc64-gnu@4.46.4':
553 | resolution: {integrity: sha512-YMdz2phOTFF+Z66dQfGf0gmeDSi5DJzY5bpZyeg9CPBkV9QDzJ1yFRlmi/j7WWRf3hYIWrOaJj5jsfwgc8GTHQ==}
554 | cpu: [ppc64]
555 | os: [linux]
556 |
557 | '@rollup/rollup-linux-riscv64-gnu@4.46.4':
558 | resolution: {integrity: sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==}
559 | cpu: [riscv64]
560 | os: [linux]
561 |
562 | '@rollup/rollup-linux-riscv64-musl@4.46.4':
563 | resolution: {integrity: sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==}
564 | cpu: [riscv64]
565 | os: [linux]
566 |
567 | '@rollup/rollup-linux-s390x-gnu@4.46.4':
568 | resolution: {integrity: sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==}
569 | cpu: [s390x]
570 | os: [linux]
571 |
572 | '@rollup/rollup-linux-x64-gnu@4.46.4':
573 | resolution: {integrity: sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==}
574 | cpu: [x64]
575 | os: [linux]
576 |
577 | '@rollup/rollup-linux-x64-musl@4.46.4':
578 | resolution: {integrity: sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==}
579 | cpu: [x64]
580 | os: [linux]
581 |
582 | '@rollup/rollup-win32-arm64-msvc@4.46.4':
583 | resolution: {integrity: sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==}
584 | cpu: [arm64]
585 | os: [win32]
586 |
587 | '@rollup/rollup-win32-ia32-msvc@4.46.4':
588 | resolution: {integrity: sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==}
589 | cpu: [ia32]
590 | os: [win32]
591 |
592 | '@rollup/rollup-win32-x64-msvc@4.46.4':
593 | resolution: {integrity: sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==}
594 | cpu: [x64]
595 | os: [win32]
596 |
597 | '@rushstack/node-core-library@5.14.0':
598 | resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==}
599 | peerDependencies:
600 | '@types/node': '*'
601 | peerDependenciesMeta:
602 | '@types/node':
603 | optional: true
604 |
605 | '@rushstack/rig-package@0.5.3':
606 | resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==}
607 |
608 | '@rushstack/terminal@0.15.4':
609 | resolution: {integrity: sha512-OQSThV0itlwVNHV6thoXiAYZlQh4Fgvie2CzxFABsbO2MWQsI4zOh3LRNigYSTrmS+ba2j0B3EObakPzf/x6Zg==}
610 | peerDependencies:
611 | '@types/node': '*'
612 | peerDependenciesMeta:
613 | '@types/node':
614 | optional: true
615 |
616 | '@rushstack/ts-command-line@5.0.2':
617 | resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==}
618 |
619 | '@shikijs/engine-oniguruma@3.17.0':
620 | resolution: {integrity: sha512-flSbHZAiOZDNTrEbULY8DLWavu/TyVu/E7RChpLB4WvKX4iHMfj80C6Hi3TjIWaQtHOW0KC6kzMcuB5TO1hZ8Q==}
621 |
622 | '@shikijs/langs@3.17.0':
623 | resolution: {integrity: sha512-icmur2n5Ojb+HAiQu6NEcIIJ8oWDFGGEpiqSCe43539Sabpx7Y829WR3QuUW2zjTM4l6V8Sazgb3rrHO2orEAw==}
624 |
625 | '@shikijs/themes@3.17.0':
626 | resolution: {integrity: sha512-/xEizMHLBmMHwtx4JuOkRf3zwhWD2bmG5BRr0IPjpcWpaq4C3mYEuTk/USAEglN0qPrTwEHwKVpSu/y2jhferA==}
627 |
628 | '@shikijs/types@3.17.0':
629 | resolution: {integrity: sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==}
630 |
631 | '@shikijs/vscode-textmate@10.0.2':
632 | resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
633 |
634 | '@types/argparse@1.0.38':
635 | resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
636 |
637 | '@types/babel__core@7.20.5':
638 | resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
639 |
640 | '@types/babel__generator@7.27.0':
641 | resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
642 |
643 | '@types/babel__template@7.4.4':
644 | resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
645 |
646 | '@types/babel__traverse@7.28.0':
647 | resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
648 |
649 | '@types/estree@1.0.8':
650 | resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
651 |
652 | '@types/hast@3.0.4':
653 | resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
654 |
655 | '@types/node@24.9.1':
656 | resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
657 |
658 | '@types/react-dom@19.2.3':
659 | resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
660 | peerDependencies:
661 | '@types/react': ^19.2.0
662 |
663 | '@types/react@19.2.7':
664 | resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
665 |
666 | '@types/unist@3.0.3':
667 | resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
668 |
669 | '@types/webrtc@0.0.47':
670 | resolution: {integrity: sha512-l4G2skp32Q7ENiocokNhIHG6dKfws9ALJ04zW+ZZ97LMe6G+yqs/E8qsJLZJELGcdUDgEWvi2jg7KSP5TpJxeg==}
671 |
672 | '@vitejs/plugin-react@5.1.2':
673 | resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
674 | engines: {node: ^20.19.0 || >=22.12.0}
675 | peerDependencies:
676 | vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
677 |
678 | '@volar/language-core@2.4.23':
679 | resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
680 |
681 | '@volar/source-map@2.4.23':
682 | resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==}
683 |
684 | '@volar/typescript@2.4.23':
685 | resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==}
686 |
687 | '@vue/compiler-core@3.5.19':
688 | resolution: {integrity: sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==}
689 |
690 | '@vue/compiler-dom@3.5.19':
691 | resolution: {integrity: sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==}
692 |
693 | '@vue/compiler-vue2@2.7.16':
694 | resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
695 |
696 | '@vue/language-core@2.2.0':
697 | resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
698 | peerDependencies:
699 | typescript: '*'
700 | peerDependenciesMeta:
701 | typescript:
702 | optional: true
703 |
704 | '@vue/shared@3.5.19':
705 | resolution: {integrity: sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==}
706 |
707 | acorn@8.15.0:
708 | resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
709 | engines: {node: '>=0.4.0'}
710 | hasBin: true
711 |
712 | ajv-draft-04@1.0.0:
713 | resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
714 | peerDependencies:
715 | ajv: ^8.5.0
716 | peerDependenciesMeta:
717 | ajv:
718 | optional: true
719 |
720 | ajv-formats@3.0.1:
721 | resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
722 | peerDependencies:
723 | ajv: ^8.0.0
724 | peerDependenciesMeta:
725 | ajv:
726 | optional: true
727 |
728 | ajv@8.12.0:
729 | resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
730 |
731 | ajv@8.13.0:
732 | resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
733 |
734 | alien-signals@0.4.14:
735 | resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
736 |
737 | argparse@1.0.10:
738 | resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
739 |
740 | argparse@2.0.1:
741 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
742 |
743 | balanced-match@1.0.2:
744 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
745 |
746 | brace-expansion@2.0.2:
747 | resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
748 |
749 | braces@3.0.3:
750 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
751 | engines: {node: '>=8'}
752 |
753 | browserslist@4.25.3:
754 | resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
755 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
756 | hasBin: true
757 |
758 | buffer-from@1.1.2:
759 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
760 |
761 | caniuse-lite@1.0.30001735:
762 | resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
763 |
764 | chokidar@4.0.3:
765 | resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
766 | engines: {node: '>= 14.16.0'}
767 |
768 | commander@2.20.3:
769 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
770 |
771 | compare-versions@6.1.1:
772 | resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
773 |
774 | confbox@0.1.8:
775 | resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
776 |
777 | confbox@0.2.2:
778 | resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
779 |
780 | convert-source-map@2.0.0:
781 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
782 |
783 | csstype@3.2.3:
784 | resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
785 |
786 | de-indent@1.0.2:
787 | resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
788 |
789 | debug@4.4.1:
790 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
791 | engines: {node: '>=6.0'}
792 | peerDependencies:
793 | supports-color: '*'
794 | peerDependenciesMeta:
795 | supports-color:
796 | optional: true
797 |
798 | detect-libc@1.0.3:
799 | resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
800 | engines: {node: '>=0.10'}
801 | hasBin: true
802 |
803 | electron-to-chromium@1.5.207:
804 | resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==}
805 |
806 | entities@4.5.0:
807 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
808 | engines: {node: '>=0.12'}
809 |
810 | esbuild@0.27.1:
811 | resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==}
812 | engines: {node: '>=18'}
813 | hasBin: true
814 |
815 | escalade@3.2.0:
816 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
817 | engines: {node: '>=6'}
818 |
819 | estree-walker@2.0.2:
820 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
821 |
822 | exsolve@1.0.7:
823 | resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
824 |
825 | fast-deep-equal@3.1.3:
826 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
827 |
828 | fdir@6.5.0:
829 | resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
830 | engines: {node: '>=12.0.0'}
831 | peerDependencies:
832 | picomatch: ^3 || ^4
833 | peerDependenciesMeta:
834 | picomatch:
835 | optional: true
836 |
837 | fill-range@7.1.1:
838 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
839 | engines: {node: '>=8'}
840 |
841 | fs-extra@11.3.1:
842 | resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
843 | engines: {node: '>=14.14'}
844 |
845 | fsevents@2.3.2:
846 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
847 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
848 | os: [darwin]
849 |
850 | fsevents@2.3.3:
851 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
852 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
853 | os: [darwin]
854 |
855 | function-bind@1.1.2:
856 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
857 |
858 | gensync@1.0.0-beta.2:
859 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
860 | engines: {node: '>=6.9.0'}
861 |
862 | graceful-fs@4.2.11:
863 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
864 |
865 | has-flag@4.0.0:
866 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
867 | engines: {node: '>=8'}
868 |
869 | hasown@2.0.2:
870 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
871 | engines: {node: '>= 0.4'}
872 |
873 | he@1.2.0:
874 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
875 | hasBin: true
876 |
877 | immutable@5.1.3:
878 | resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
879 |
880 | import-lazy@4.0.0:
881 | resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
882 | engines: {node: '>=8'}
883 |
884 | is-core-module@2.16.1:
885 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
886 | engines: {node: '>= 0.4'}
887 |
888 | is-extglob@2.1.1:
889 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
890 | engines: {node: '>=0.10.0'}
891 |
892 | is-glob@4.0.3:
893 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
894 | engines: {node: '>=0.10.0'}
895 |
896 | is-number@7.0.0:
897 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
898 | engines: {node: '>=0.12.0'}
899 |
900 | jju@1.4.0:
901 | resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
902 |
903 | js-tokens@4.0.0:
904 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
905 |
906 | jsesc@3.1.0:
907 | resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
908 | engines: {node: '>=6'}
909 | hasBin: true
910 |
911 | json-schema-traverse@1.0.0:
912 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
913 |
914 | json5@2.2.3:
915 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
916 | engines: {node: '>=6'}
917 | hasBin: true
918 |
919 | jsonfile@6.2.0:
920 | resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
921 |
922 | kolorist@1.8.0:
923 | resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
924 |
925 | linkify-it@5.0.0:
926 | resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
927 |
928 | local-pkg@1.1.2:
929 | resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
930 | engines: {node: '>=14'}
931 |
932 | lodash@4.17.21:
933 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
934 |
935 | lru-cache@5.1.1:
936 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
937 |
938 | lru-cache@6.0.0:
939 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
940 | engines: {node: '>=10'}
941 |
942 | lunr@2.3.9:
943 | resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
944 |
945 | magic-string@0.30.17:
946 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
947 |
948 | markdown-it@14.1.0:
949 | resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
950 | hasBin: true
951 |
952 | mdurl@2.0.0:
953 | resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
954 |
955 | micromatch@4.0.8:
956 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
957 | engines: {node: '>=8.6'}
958 |
959 | minimatch@10.0.3:
960 | resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
961 | engines: {node: 20 || >=22}
962 |
963 | minimatch@9.0.5:
964 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
965 | engines: {node: '>=16 || 14 >=14.17'}
966 |
967 | mlly@1.7.4:
968 | resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
969 |
970 | ms@2.1.3:
971 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
972 |
973 | muggle-string@0.4.1:
974 | resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
975 |
976 | nanoid@3.3.11:
977 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
978 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
979 | hasBin: true
980 |
981 | node-addon-api@7.1.1:
982 | resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
983 |
984 | node-releases@2.0.19:
985 | resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
986 |
987 | path-browserify@1.0.1:
988 | resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
989 |
990 | path-parse@1.0.7:
991 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
992 |
993 | pathe@2.0.3:
994 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
995 |
996 | picocolors@1.1.1:
997 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
998 |
999 | picomatch@2.3.1:
1000 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1001 | engines: {node: '>=8.6'}
1002 |
1003 | picomatch@4.0.3:
1004 | resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
1005 | engines: {node: '>=12'}
1006 |
1007 | pkg-types@1.3.1:
1008 | resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
1009 |
1010 | pkg-types@2.3.0:
1011 | resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
1012 |
1013 | playwright-core@1.57.0:
1014 | resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
1015 | engines: {node: '>=18'}
1016 | hasBin: true
1017 |
1018 | playwright@1.57.0:
1019 | resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
1020 | engines: {node: '>=18'}
1021 | hasBin: true
1022 |
1023 | postcss@8.5.6:
1024 | resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1025 | engines: {node: ^10 || ^12 || >=14}
1026 |
1027 | punycode.js@2.3.1:
1028 | resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
1029 | engines: {node: '>=6'}
1030 |
1031 | punycode@2.3.1:
1032 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1033 | engines: {node: '>=6'}
1034 |
1035 | quansync@0.2.11:
1036 | resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
1037 |
1038 | react-dom@19.2.3:
1039 | resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
1040 | peerDependencies:
1041 | react: ^19.2.3
1042 |
1043 | react-refresh@0.18.0:
1044 | resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
1045 | engines: {node: '>=0.10.0'}
1046 |
1047 | react@19.2.3:
1048 | resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
1049 | engines: {node: '>=0.10.0'}
1050 |
1051 | readdirp@4.1.2:
1052 | resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
1053 | engines: {node: '>= 14.18.0'}
1054 |
1055 | require-from-string@2.0.2:
1056 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
1057 | engines: {node: '>=0.10.0'}
1058 |
1059 | resolve@1.22.10:
1060 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
1061 | engines: {node: '>= 0.4'}
1062 | hasBin: true
1063 |
1064 | rollup@4.46.4:
1065 | resolution: {integrity: sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==}
1066 | engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1067 | hasBin: true
1068 |
1069 | sass@1.83.0:
1070 | resolution: {integrity: sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==}
1071 | engines: {node: '>=14.0.0'}
1072 | hasBin: true
1073 |
1074 | scheduler@0.27.0:
1075 | resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
1076 |
1077 | semver@6.3.1:
1078 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1079 | hasBin: true
1080 |
1081 | semver@7.5.4:
1082 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
1083 | engines: {node: '>=10'}
1084 | hasBin: true
1085 |
1086 | source-map-js@1.2.1:
1087 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1088 | engines: {node: '>=0.10.0'}
1089 |
1090 | source-map-support@0.5.21:
1091 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
1092 |
1093 | source-map@0.6.1:
1094 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
1095 | engines: {node: '>=0.10.0'}
1096 |
1097 | sprintf-js@1.0.3:
1098 | resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
1099 |
1100 | string-argv@0.3.2:
1101 | resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
1102 | engines: {node: '>=0.6.19'}
1103 |
1104 | strip-json-comments@3.1.1:
1105 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
1106 | engines: {node: '>=8'}
1107 |
1108 | supports-color@8.1.1:
1109 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
1110 | engines: {node: '>=10'}
1111 |
1112 | supports-preserve-symlinks-flag@1.0.0:
1113 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
1114 | engines: {node: '>= 0.4'}
1115 |
1116 | terser@5.37.0:
1117 | resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==}
1118 | engines: {node: '>=10'}
1119 | hasBin: true
1120 |
1121 | tinyglobby@0.2.15:
1122 | resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
1123 | engines: {node: '>=12.0.0'}
1124 |
1125 | to-regex-range@5.0.1:
1126 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1127 | engines: {node: '>=8.0'}
1128 |
1129 | typedoc@0.28.15:
1130 | resolution: {integrity: sha512-mw2/2vTL7MlT+BVo43lOsufkkd2CJO4zeOSuWQQsiXoV2VuEn7f6IZp2jsUDPmBMABpgR0R5jlcJ2OGEFYmkyg==}
1131 | engines: {node: '>= 18', pnpm: '>= 10'}
1132 | hasBin: true
1133 | peerDependencies:
1134 | typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x
1135 |
1136 | typescript@5.8.2:
1137 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
1138 | engines: {node: '>=14.17'}
1139 | hasBin: true
1140 |
1141 | typescript@5.9.3:
1142 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
1143 | engines: {node: '>=14.17'}
1144 | hasBin: true
1145 |
1146 | uc.micro@2.1.0:
1147 | resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
1148 |
1149 | ufo@1.6.1:
1150 | resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
1151 |
1152 | undici-types@7.16.0:
1153 | resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
1154 |
1155 | universalify@2.0.1:
1156 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
1157 | engines: {node: '>= 10.0.0'}
1158 |
1159 | update-browserslist-db@1.1.3:
1160 | resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1161 | hasBin: true
1162 | peerDependencies:
1163 | browserslist: '>= 4.21.0'
1164 |
1165 | uri-js@4.4.1:
1166 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1167 |
1168 | vite-plugin-dts@4.5.4:
1169 | resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==}
1170 | peerDependencies:
1171 | typescript: '*'
1172 | vite: '*'
1173 | peerDependenciesMeta:
1174 | vite:
1175 | optional: true
1176 |
1177 | vite@7.3.0:
1178 | resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
1179 | engines: {node: ^20.19.0 || >=22.12.0}
1180 | hasBin: true
1181 | peerDependencies:
1182 | '@types/node': ^20.19.0 || >=22.12.0
1183 | jiti: '>=1.21.0'
1184 | less: ^4.0.0
1185 | lightningcss: ^1.21.0
1186 | sass: ^1.70.0
1187 | sass-embedded: ^1.70.0
1188 | stylus: '>=0.54.8'
1189 | sugarss: ^5.0.0
1190 | terser: ^5.16.0
1191 | tsx: ^4.8.1
1192 | yaml: ^2.4.2
1193 | peerDependenciesMeta:
1194 | '@types/node':
1195 | optional: true
1196 | jiti:
1197 | optional: true
1198 | less:
1199 | optional: true
1200 | lightningcss:
1201 | optional: true
1202 | sass:
1203 | optional: true
1204 | sass-embedded:
1205 | optional: true
1206 | stylus:
1207 | optional: true
1208 | sugarss:
1209 | optional: true
1210 | terser:
1211 | optional: true
1212 | tsx:
1213 | optional: true
1214 | yaml:
1215 | optional: true
1216 |
1217 | vscode-uri@3.1.0:
1218 | resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
1219 |
1220 | yallist@3.1.1:
1221 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1222 |
1223 | yallist@4.0.0:
1224 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
1225 |
1226 | yaml@2.8.1:
1227 | resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
1228 | engines: {node: '>= 14.6'}
1229 | hasBin: true
1230 |
1231 | zustand@5.0.9:
1232 | resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==}
1233 | engines: {node: '>=12.20.0'}
1234 | peerDependencies:
1235 | '@types/react': '>=18.0.0'
1236 | immer: '>=9.0.6'
1237 | react: '>=18.0.0'
1238 | use-sync-external-store: '>=1.2.0'
1239 | peerDependenciesMeta:
1240 | '@types/react':
1241 | optional: true
1242 | immer:
1243 | optional: true
1244 | react:
1245 | optional: true
1246 | use-sync-external-store:
1247 | optional: true
1248 |
1249 | snapshots:
1250 |
1251 | '@babel/code-frame@7.27.1':
1252 | dependencies:
1253 | '@babel/helper-validator-identifier': 7.28.5
1254 | js-tokens: 4.0.0
1255 | picocolors: 1.1.1
1256 |
1257 | '@babel/compat-data@7.28.0': {}
1258 |
1259 | '@babel/core@7.28.5':
1260 | dependencies:
1261 | '@babel/code-frame': 7.27.1
1262 | '@babel/generator': 7.28.5
1263 | '@babel/helper-compilation-targets': 7.27.2
1264 | '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
1265 | '@babel/helpers': 7.28.4
1266 | '@babel/parser': 7.28.5
1267 | '@babel/template': 7.27.2
1268 | '@babel/traverse': 7.28.5
1269 | '@babel/types': 7.28.5
1270 | '@jridgewell/remapping': 2.3.5
1271 | convert-source-map: 2.0.0
1272 | debug: 4.4.1
1273 | gensync: 1.0.0-beta.2
1274 | json5: 2.2.3
1275 | semver: 6.3.1
1276 | transitivePeerDependencies:
1277 | - supports-color
1278 |
1279 | '@babel/generator@7.28.5':
1280 | dependencies:
1281 | '@babel/parser': 7.28.5
1282 | '@babel/types': 7.28.5
1283 | '@jridgewell/gen-mapping': 0.3.13
1284 | '@jridgewell/trace-mapping': 0.3.30
1285 | jsesc: 3.1.0
1286 |
1287 | '@babel/helper-compilation-targets@7.27.2':
1288 | dependencies:
1289 | '@babel/compat-data': 7.28.0
1290 | '@babel/helper-validator-option': 7.27.1
1291 | browserslist: 4.25.3
1292 | lru-cache: 5.1.1
1293 | semver: 6.3.1
1294 |
1295 | '@babel/helper-globals@7.28.0': {}
1296 |
1297 | '@babel/helper-module-imports@7.27.1':
1298 | dependencies:
1299 | '@babel/traverse': 7.28.5
1300 | '@babel/types': 7.28.5
1301 | transitivePeerDependencies:
1302 | - supports-color
1303 |
1304 | '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
1305 | dependencies:
1306 | '@babel/core': 7.28.5
1307 | '@babel/helper-module-imports': 7.27.1
1308 | '@babel/helper-validator-identifier': 7.28.5
1309 | '@babel/traverse': 7.28.5
1310 | transitivePeerDependencies:
1311 | - supports-color
1312 |
1313 | '@babel/helper-plugin-utils@7.27.1': {}
1314 |
1315 | '@babel/helper-string-parser@7.27.1': {}
1316 |
1317 | '@babel/helper-validator-identifier@7.28.5': {}
1318 |
1319 | '@babel/helper-validator-option@7.27.1': {}
1320 |
1321 | '@babel/helpers@7.28.4':
1322 | dependencies:
1323 | '@babel/template': 7.27.2
1324 | '@babel/types': 7.28.5
1325 |
1326 | '@babel/parser@7.28.5':
1327 | dependencies:
1328 | '@babel/types': 7.28.5
1329 |
1330 | '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
1331 | dependencies:
1332 | '@babel/core': 7.28.5
1333 | '@babel/helper-plugin-utils': 7.27.1
1334 |
1335 | '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
1336 | dependencies:
1337 | '@babel/core': 7.28.5
1338 | '@babel/helper-plugin-utils': 7.27.1
1339 |
1340 | '@babel/template@7.27.2':
1341 | dependencies:
1342 | '@babel/code-frame': 7.27.1
1343 | '@babel/parser': 7.28.5
1344 | '@babel/types': 7.28.5
1345 |
1346 | '@babel/traverse@7.28.5':
1347 | dependencies:
1348 | '@babel/code-frame': 7.27.1
1349 | '@babel/generator': 7.28.5
1350 | '@babel/helper-globals': 7.28.0
1351 | '@babel/parser': 7.28.5
1352 | '@babel/template': 7.27.2
1353 | '@babel/types': 7.28.5
1354 | debug: 4.4.1
1355 | transitivePeerDependencies:
1356 | - supports-color
1357 |
1358 | '@babel/types@7.28.5':
1359 | dependencies:
1360 | '@babel/helper-string-parser': 7.27.1
1361 | '@babel/helper-validator-identifier': 7.28.5
1362 |
1363 | '@biomejs/biome@2.3.10':
1364 | optionalDependencies:
1365 | '@biomejs/cli-darwin-arm64': 2.3.10
1366 | '@biomejs/cli-darwin-x64': 2.3.10
1367 | '@biomejs/cli-linux-arm64': 2.3.10
1368 | '@biomejs/cli-linux-arm64-musl': 2.3.10
1369 | '@biomejs/cli-linux-x64': 2.3.10
1370 | '@biomejs/cli-linux-x64-musl': 2.3.10
1371 | '@biomejs/cli-win32-arm64': 2.3.10
1372 | '@biomejs/cli-win32-x64': 2.3.10
1373 |
1374 | '@biomejs/cli-darwin-arm64@2.3.10':
1375 | optional: true
1376 |
1377 | '@biomejs/cli-darwin-x64@2.3.10':
1378 | optional: true
1379 |
1380 | '@biomejs/cli-linux-arm64-musl@2.3.10':
1381 | optional: true
1382 |
1383 | '@biomejs/cli-linux-arm64@2.3.10':
1384 | optional: true
1385 |
1386 | '@biomejs/cli-linux-x64-musl@2.3.10':
1387 | optional: true
1388 |
1389 | '@biomejs/cli-linux-x64@2.3.10':
1390 | optional: true
1391 |
1392 | '@biomejs/cli-win32-arm64@2.3.10':
1393 | optional: true
1394 |
1395 | '@biomejs/cli-win32-x64@2.3.10':
1396 | optional: true
1397 |
1398 | '@esbuild/aix-ppc64@0.27.1':
1399 | optional: true
1400 |
1401 | '@esbuild/android-arm64@0.27.1':
1402 | optional: true
1403 |
1404 | '@esbuild/android-arm@0.27.1':
1405 | optional: true
1406 |
1407 | '@esbuild/android-x64@0.27.1':
1408 | optional: true
1409 |
1410 | '@esbuild/darwin-arm64@0.27.1':
1411 | optional: true
1412 |
1413 | '@esbuild/darwin-x64@0.27.1':
1414 | optional: true
1415 |
1416 | '@esbuild/freebsd-arm64@0.27.1':
1417 | optional: true
1418 |
1419 | '@esbuild/freebsd-x64@0.27.1':
1420 | optional: true
1421 |
1422 | '@esbuild/linux-arm64@0.27.1':
1423 | optional: true
1424 |
1425 | '@esbuild/linux-arm@0.27.1':
1426 | optional: true
1427 |
1428 | '@esbuild/linux-ia32@0.27.1':
1429 | optional: true
1430 |
1431 | '@esbuild/linux-loong64@0.27.1':
1432 | optional: true
1433 |
1434 | '@esbuild/linux-mips64el@0.27.1':
1435 | optional: true
1436 |
1437 | '@esbuild/linux-ppc64@0.27.1':
1438 | optional: true
1439 |
1440 | '@esbuild/linux-riscv64@0.27.1':
1441 | optional: true
1442 |
1443 | '@esbuild/linux-s390x@0.27.1':
1444 | optional: true
1445 |
1446 | '@esbuild/linux-x64@0.27.1':
1447 | optional: true
1448 |
1449 | '@esbuild/netbsd-arm64@0.27.1':
1450 | optional: true
1451 |
1452 | '@esbuild/netbsd-x64@0.27.1':
1453 | optional: true
1454 |
1455 | '@esbuild/openbsd-arm64@0.27.1':
1456 | optional: true
1457 |
1458 | '@esbuild/openbsd-x64@0.27.1':
1459 | optional: true
1460 |
1461 | '@esbuild/openharmony-arm64@0.27.1':
1462 | optional: true
1463 |
1464 | '@esbuild/sunos-x64@0.27.1':
1465 | optional: true
1466 |
1467 | '@esbuild/win32-arm64@0.27.1':
1468 | optional: true
1469 |
1470 | '@esbuild/win32-ia32@0.27.1':
1471 | optional: true
1472 |
1473 | '@esbuild/win32-x64@0.27.1':
1474 | optional: true
1475 |
1476 | '@gerrit0/mini-shiki@3.17.0':
1477 | dependencies:
1478 | '@shikijs/engine-oniguruma': 3.17.0
1479 | '@shikijs/langs': 3.17.0
1480 | '@shikijs/themes': 3.17.0
1481 | '@shikijs/types': 3.17.0
1482 | '@shikijs/vscode-textmate': 10.0.2
1483 |
1484 | '@isaacs/balanced-match@4.0.1': {}
1485 |
1486 | '@isaacs/brace-expansion@5.0.0':
1487 | dependencies:
1488 | '@isaacs/balanced-match': 4.0.1
1489 |
1490 | '@jridgewell/gen-mapping@0.3.13':
1491 | dependencies:
1492 | '@jridgewell/sourcemap-codec': 1.5.5
1493 | '@jridgewell/trace-mapping': 0.3.30
1494 |
1495 | '@jridgewell/remapping@2.3.5':
1496 | dependencies:
1497 | '@jridgewell/gen-mapping': 0.3.13
1498 | '@jridgewell/trace-mapping': 0.3.30
1499 |
1500 | '@jridgewell/resolve-uri@3.1.2': {}
1501 |
1502 | '@jridgewell/source-map@0.3.11':
1503 | dependencies:
1504 | '@jridgewell/gen-mapping': 0.3.13
1505 | '@jridgewell/trace-mapping': 0.3.30
1506 | optional: true
1507 |
1508 | '@jridgewell/sourcemap-codec@1.5.5': {}
1509 |
1510 | '@jridgewell/trace-mapping@0.3.30':
1511 | dependencies:
1512 | '@jridgewell/resolve-uri': 3.1.2
1513 | '@jridgewell/sourcemap-codec': 1.5.5
1514 |
1515 | '@microsoft/api-extractor-model@7.30.7(@types/node@24.9.1)':
1516 | dependencies:
1517 | '@microsoft/tsdoc': 0.15.1
1518 | '@microsoft/tsdoc-config': 0.17.1
1519 | '@rushstack/node-core-library': 5.14.0(@types/node@24.9.1)
1520 | transitivePeerDependencies:
1521 | - '@types/node'
1522 |
1523 | '@microsoft/api-extractor@7.52.11(@types/node@24.9.1)':
1524 | dependencies:
1525 | '@microsoft/api-extractor-model': 7.30.7(@types/node@24.9.1)
1526 | '@microsoft/tsdoc': 0.15.1
1527 | '@microsoft/tsdoc-config': 0.17.1
1528 | '@rushstack/node-core-library': 5.14.0(@types/node@24.9.1)
1529 | '@rushstack/rig-package': 0.5.3
1530 | '@rushstack/terminal': 0.15.4(@types/node@24.9.1)
1531 | '@rushstack/ts-command-line': 5.0.2(@types/node@24.9.1)
1532 | lodash: 4.17.21
1533 | minimatch: 10.0.3
1534 | resolve: 1.22.10
1535 | semver: 7.5.4
1536 | source-map: 0.6.1
1537 | typescript: 5.8.2
1538 | transitivePeerDependencies:
1539 | - '@types/node'
1540 |
1541 | '@microsoft/tsdoc-config@0.17.1':
1542 | dependencies:
1543 | '@microsoft/tsdoc': 0.15.1
1544 | ajv: 8.12.0
1545 | jju: 1.4.0
1546 | resolve: 1.22.10
1547 |
1548 | '@microsoft/tsdoc@0.15.1': {}
1549 |
1550 | '@parcel/watcher-android-arm64@2.5.1':
1551 | optional: true
1552 |
1553 | '@parcel/watcher-darwin-arm64@2.5.1':
1554 | optional: true
1555 |
1556 | '@parcel/watcher-darwin-x64@2.5.1':
1557 | optional: true
1558 |
1559 | '@parcel/watcher-freebsd-x64@2.5.1':
1560 | optional: true
1561 |
1562 | '@parcel/watcher-linux-arm-glibc@2.5.1':
1563 | optional: true
1564 |
1565 | '@parcel/watcher-linux-arm-musl@2.5.1':
1566 | optional: true
1567 |
1568 | '@parcel/watcher-linux-arm64-glibc@2.5.1':
1569 | optional: true
1570 |
1571 | '@parcel/watcher-linux-arm64-musl@2.5.1':
1572 | optional: true
1573 |
1574 | '@parcel/watcher-linux-x64-glibc@2.5.1':
1575 | optional: true
1576 |
1577 | '@parcel/watcher-linux-x64-musl@2.5.1':
1578 | optional: true
1579 |
1580 | '@parcel/watcher-win32-arm64@2.5.1':
1581 | optional: true
1582 |
1583 | '@parcel/watcher-win32-ia32@2.5.1':
1584 | optional: true
1585 |
1586 | '@parcel/watcher-win32-x64@2.5.1':
1587 | optional: true
1588 |
1589 | '@parcel/watcher@2.5.1':
1590 | dependencies:
1591 | detect-libc: 1.0.3
1592 | is-glob: 4.0.3
1593 | micromatch: 4.0.8
1594 | node-addon-api: 7.1.1
1595 | optionalDependencies:
1596 | '@parcel/watcher-android-arm64': 2.5.1
1597 | '@parcel/watcher-darwin-arm64': 2.5.1
1598 | '@parcel/watcher-darwin-x64': 2.5.1
1599 | '@parcel/watcher-freebsd-x64': 2.5.1
1600 | '@parcel/watcher-linux-arm-glibc': 2.5.1
1601 | '@parcel/watcher-linux-arm-musl': 2.5.1
1602 | '@parcel/watcher-linux-arm64-glibc': 2.5.1
1603 | '@parcel/watcher-linux-arm64-musl': 2.5.1
1604 | '@parcel/watcher-linux-x64-glibc': 2.5.1
1605 | '@parcel/watcher-linux-x64-musl': 2.5.1
1606 | '@parcel/watcher-win32-arm64': 2.5.1
1607 | '@parcel/watcher-win32-ia32': 2.5.1
1608 | '@parcel/watcher-win32-x64': 2.5.1
1609 | optional: true
1610 |
1611 | '@playwright/test@1.57.0':
1612 | dependencies:
1613 | playwright: 1.57.0
1614 |
1615 | '@rolldown/pluginutils@1.0.0-beta.53': {}
1616 |
1617 | '@rollup/pluginutils@5.2.0(rollup@4.46.4)':
1618 | dependencies:
1619 | '@types/estree': 1.0.8
1620 | estree-walker: 2.0.2
1621 | picomatch: 4.0.3
1622 | optionalDependencies:
1623 | rollup: 4.46.4
1624 |
1625 | '@rollup/rollup-android-arm-eabi@4.46.4':
1626 | optional: true
1627 |
1628 | '@rollup/rollup-android-arm64@4.46.4':
1629 | optional: true
1630 |
1631 | '@rollup/rollup-darwin-arm64@4.46.4':
1632 | optional: true
1633 |
1634 | '@rollup/rollup-darwin-x64@4.46.4':
1635 | optional: true
1636 |
1637 | '@rollup/rollup-freebsd-arm64@4.46.4':
1638 | optional: true
1639 |
1640 | '@rollup/rollup-freebsd-x64@4.46.4':
1641 | optional: true
1642 |
1643 | '@rollup/rollup-linux-arm-gnueabihf@4.46.4':
1644 | optional: true
1645 |
1646 | '@rollup/rollup-linux-arm-musleabihf@4.46.4':
1647 | optional: true
1648 |
1649 | '@rollup/rollup-linux-arm64-gnu@4.46.4':
1650 | optional: true
1651 |
1652 | '@rollup/rollup-linux-arm64-musl@4.46.4':
1653 | optional: true
1654 |
1655 | '@rollup/rollup-linux-loongarch64-gnu@4.46.4':
1656 | optional: true
1657 |
1658 | '@rollup/rollup-linux-ppc64-gnu@4.46.4':
1659 | optional: true
1660 |
1661 | '@rollup/rollup-linux-riscv64-gnu@4.46.4':
1662 | optional: true
1663 |
1664 | '@rollup/rollup-linux-riscv64-musl@4.46.4':
1665 | optional: true
1666 |
1667 | '@rollup/rollup-linux-s390x-gnu@4.46.4':
1668 | optional: true
1669 |
1670 | '@rollup/rollup-linux-x64-gnu@4.46.4':
1671 | optional: true
1672 |
1673 | '@rollup/rollup-linux-x64-musl@4.46.4':
1674 | optional: true
1675 |
1676 | '@rollup/rollup-win32-arm64-msvc@4.46.4':
1677 | optional: true
1678 |
1679 | '@rollup/rollup-win32-ia32-msvc@4.46.4':
1680 | optional: true
1681 |
1682 | '@rollup/rollup-win32-x64-msvc@4.46.4':
1683 | optional: true
1684 |
1685 | '@rushstack/node-core-library@5.14.0(@types/node@24.9.1)':
1686 | dependencies:
1687 | ajv: 8.13.0
1688 | ajv-draft-04: 1.0.0(ajv@8.13.0)
1689 | ajv-formats: 3.0.1(ajv@8.13.0)
1690 | fs-extra: 11.3.1
1691 | import-lazy: 4.0.0
1692 | jju: 1.4.0
1693 | resolve: 1.22.10
1694 | semver: 7.5.4
1695 | optionalDependencies:
1696 | '@types/node': 24.9.1
1697 |
1698 | '@rushstack/rig-package@0.5.3':
1699 | dependencies:
1700 | resolve: 1.22.10
1701 | strip-json-comments: 3.1.1
1702 |
1703 | '@rushstack/terminal@0.15.4(@types/node@24.9.1)':
1704 | dependencies:
1705 | '@rushstack/node-core-library': 5.14.0(@types/node@24.9.1)
1706 | supports-color: 8.1.1
1707 | optionalDependencies:
1708 | '@types/node': 24.9.1
1709 |
1710 | '@rushstack/ts-command-line@5.0.2(@types/node@24.9.1)':
1711 | dependencies:
1712 | '@rushstack/terminal': 0.15.4(@types/node@24.9.1)
1713 | '@types/argparse': 1.0.38
1714 | argparse: 1.0.10
1715 | string-argv: 0.3.2
1716 | transitivePeerDependencies:
1717 | - '@types/node'
1718 |
1719 | '@shikijs/engine-oniguruma@3.17.0':
1720 | dependencies:
1721 | '@shikijs/types': 3.17.0
1722 | '@shikijs/vscode-textmate': 10.0.2
1723 |
1724 | '@shikijs/langs@3.17.0':
1725 | dependencies:
1726 | '@shikijs/types': 3.17.0
1727 |
1728 | '@shikijs/themes@3.17.0':
1729 | dependencies:
1730 | '@shikijs/types': 3.17.0
1731 |
1732 | '@shikijs/types@3.17.0':
1733 | dependencies:
1734 | '@shikijs/vscode-textmate': 10.0.2
1735 | '@types/hast': 3.0.4
1736 |
1737 | '@shikijs/vscode-textmate@10.0.2': {}
1738 |
1739 | '@types/argparse@1.0.38': {}
1740 |
1741 | '@types/babel__core@7.20.5':
1742 | dependencies:
1743 | '@babel/parser': 7.28.5
1744 | '@babel/types': 7.28.5
1745 | '@types/babel__generator': 7.27.0
1746 | '@types/babel__template': 7.4.4
1747 | '@types/babel__traverse': 7.28.0
1748 |
1749 | '@types/babel__generator@7.27.0':
1750 | dependencies:
1751 | '@babel/types': 7.28.5
1752 |
1753 | '@types/babel__template@7.4.4':
1754 | dependencies:
1755 | '@babel/parser': 7.28.5
1756 | '@babel/types': 7.28.5
1757 |
1758 | '@types/babel__traverse@7.28.0':
1759 | dependencies:
1760 | '@babel/types': 7.28.5
1761 |
1762 | '@types/estree@1.0.8': {}
1763 |
1764 | '@types/hast@3.0.4':
1765 | dependencies:
1766 | '@types/unist': 3.0.3
1767 |
1768 | '@types/node@24.9.1':
1769 | dependencies:
1770 | undici-types: 7.16.0
1771 |
1772 | '@types/react-dom@19.2.3(@types/react@19.2.7)':
1773 | dependencies:
1774 | '@types/react': 19.2.7
1775 |
1776 | '@types/react@19.2.7':
1777 | dependencies:
1778 | csstype: 3.2.3
1779 |
1780 | '@types/unist@3.0.3': {}
1781 |
1782 | '@types/webrtc@0.0.47': {}
1783 |
1784 | '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1))':
1785 | dependencies:
1786 | '@babel/core': 7.28.5
1787 | '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
1788 | '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
1789 | '@rolldown/pluginutils': 1.0.0-beta.53
1790 | '@types/babel__core': 7.20.5
1791 | react-refresh: 0.18.0
1792 | vite: 7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1)
1793 | transitivePeerDependencies:
1794 | - supports-color
1795 |
1796 | '@volar/language-core@2.4.23':
1797 | dependencies:
1798 | '@volar/source-map': 2.4.23
1799 |
1800 | '@volar/source-map@2.4.23': {}
1801 |
1802 | '@volar/typescript@2.4.23':
1803 | dependencies:
1804 | '@volar/language-core': 2.4.23
1805 | path-browserify: 1.0.1
1806 | vscode-uri: 3.1.0
1807 |
1808 | '@vue/compiler-core@3.5.19':
1809 | dependencies:
1810 | '@babel/parser': 7.28.5
1811 | '@vue/shared': 3.5.19
1812 | entities: 4.5.0
1813 | estree-walker: 2.0.2
1814 | source-map-js: 1.2.1
1815 |
1816 | '@vue/compiler-dom@3.5.19':
1817 | dependencies:
1818 | '@vue/compiler-core': 3.5.19
1819 | '@vue/shared': 3.5.19
1820 |
1821 | '@vue/compiler-vue2@2.7.16':
1822 | dependencies:
1823 | de-indent: 1.0.2
1824 | he: 1.2.0
1825 |
1826 | '@vue/language-core@2.2.0(typescript@5.9.3)':
1827 | dependencies:
1828 | '@volar/language-core': 2.4.23
1829 | '@vue/compiler-dom': 3.5.19
1830 | '@vue/compiler-vue2': 2.7.16
1831 | '@vue/shared': 3.5.19
1832 | alien-signals: 0.4.14
1833 | minimatch: 9.0.5
1834 | muggle-string: 0.4.1
1835 | path-browserify: 1.0.1
1836 | optionalDependencies:
1837 | typescript: 5.9.3
1838 |
1839 | '@vue/shared@3.5.19': {}
1840 |
1841 | acorn@8.15.0: {}
1842 |
1843 | ajv-draft-04@1.0.0(ajv@8.13.0):
1844 | optionalDependencies:
1845 | ajv: 8.13.0
1846 |
1847 | ajv-formats@3.0.1(ajv@8.13.0):
1848 | optionalDependencies:
1849 | ajv: 8.13.0
1850 |
1851 | ajv@8.12.0:
1852 | dependencies:
1853 | fast-deep-equal: 3.1.3
1854 | json-schema-traverse: 1.0.0
1855 | require-from-string: 2.0.2
1856 | uri-js: 4.4.1
1857 |
1858 | ajv@8.13.0:
1859 | dependencies:
1860 | fast-deep-equal: 3.1.3
1861 | json-schema-traverse: 1.0.0
1862 | require-from-string: 2.0.2
1863 | uri-js: 4.4.1
1864 |
1865 | alien-signals@0.4.14: {}
1866 |
1867 | argparse@1.0.10:
1868 | dependencies:
1869 | sprintf-js: 1.0.3
1870 |
1871 | argparse@2.0.1: {}
1872 |
1873 | balanced-match@1.0.2: {}
1874 |
1875 | brace-expansion@2.0.2:
1876 | dependencies:
1877 | balanced-match: 1.0.2
1878 |
1879 | braces@3.0.3:
1880 | dependencies:
1881 | fill-range: 7.1.1
1882 | optional: true
1883 |
1884 | browserslist@4.25.3:
1885 | dependencies:
1886 | caniuse-lite: 1.0.30001735
1887 | electron-to-chromium: 1.5.207
1888 | node-releases: 2.0.19
1889 | update-browserslist-db: 1.1.3(browserslist@4.25.3)
1890 |
1891 | buffer-from@1.1.2:
1892 | optional: true
1893 |
1894 | caniuse-lite@1.0.30001735: {}
1895 |
1896 | chokidar@4.0.3:
1897 | dependencies:
1898 | readdirp: 4.1.2
1899 | optional: true
1900 |
1901 | commander@2.20.3:
1902 | optional: true
1903 |
1904 | compare-versions@6.1.1: {}
1905 |
1906 | confbox@0.1.8: {}
1907 |
1908 | confbox@0.2.2: {}
1909 |
1910 | convert-source-map@2.0.0: {}
1911 |
1912 | csstype@3.2.3: {}
1913 |
1914 | de-indent@1.0.2: {}
1915 |
1916 | debug@4.4.1:
1917 | dependencies:
1918 | ms: 2.1.3
1919 |
1920 | detect-libc@1.0.3:
1921 | optional: true
1922 |
1923 | electron-to-chromium@1.5.207: {}
1924 |
1925 | entities@4.5.0: {}
1926 |
1927 | esbuild@0.27.1:
1928 | optionalDependencies:
1929 | '@esbuild/aix-ppc64': 0.27.1
1930 | '@esbuild/android-arm': 0.27.1
1931 | '@esbuild/android-arm64': 0.27.1
1932 | '@esbuild/android-x64': 0.27.1
1933 | '@esbuild/darwin-arm64': 0.27.1
1934 | '@esbuild/darwin-x64': 0.27.1
1935 | '@esbuild/freebsd-arm64': 0.27.1
1936 | '@esbuild/freebsd-x64': 0.27.1
1937 | '@esbuild/linux-arm': 0.27.1
1938 | '@esbuild/linux-arm64': 0.27.1
1939 | '@esbuild/linux-ia32': 0.27.1
1940 | '@esbuild/linux-loong64': 0.27.1
1941 | '@esbuild/linux-mips64el': 0.27.1
1942 | '@esbuild/linux-ppc64': 0.27.1
1943 | '@esbuild/linux-riscv64': 0.27.1
1944 | '@esbuild/linux-s390x': 0.27.1
1945 | '@esbuild/linux-x64': 0.27.1
1946 | '@esbuild/netbsd-arm64': 0.27.1
1947 | '@esbuild/netbsd-x64': 0.27.1
1948 | '@esbuild/openbsd-arm64': 0.27.1
1949 | '@esbuild/openbsd-x64': 0.27.1
1950 | '@esbuild/openharmony-arm64': 0.27.1
1951 | '@esbuild/sunos-x64': 0.27.1
1952 | '@esbuild/win32-arm64': 0.27.1
1953 | '@esbuild/win32-ia32': 0.27.1
1954 | '@esbuild/win32-x64': 0.27.1
1955 |
1956 | escalade@3.2.0: {}
1957 |
1958 | estree-walker@2.0.2: {}
1959 |
1960 | exsolve@1.0.7: {}
1961 |
1962 | fast-deep-equal@3.1.3: {}
1963 |
1964 | fdir@6.5.0(picomatch@4.0.3):
1965 | optionalDependencies:
1966 | picomatch: 4.0.3
1967 |
1968 | fill-range@7.1.1:
1969 | dependencies:
1970 | to-regex-range: 5.0.1
1971 | optional: true
1972 |
1973 | fs-extra@11.3.1:
1974 | dependencies:
1975 | graceful-fs: 4.2.11
1976 | jsonfile: 6.2.0
1977 | universalify: 2.0.1
1978 |
1979 | fsevents@2.3.2:
1980 | optional: true
1981 |
1982 | fsevents@2.3.3:
1983 | optional: true
1984 |
1985 | function-bind@1.1.2: {}
1986 |
1987 | gensync@1.0.0-beta.2: {}
1988 |
1989 | graceful-fs@4.2.11: {}
1990 |
1991 | has-flag@4.0.0: {}
1992 |
1993 | hasown@2.0.2:
1994 | dependencies:
1995 | function-bind: 1.1.2
1996 |
1997 | he@1.2.0: {}
1998 |
1999 | immutable@5.1.3:
2000 | optional: true
2001 |
2002 | import-lazy@4.0.0: {}
2003 |
2004 | is-core-module@2.16.1:
2005 | dependencies:
2006 | hasown: 2.0.2
2007 |
2008 | is-extglob@2.1.1:
2009 | optional: true
2010 |
2011 | is-glob@4.0.3:
2012 | dependencies:
2013 | is-extglob: 2.1.1
2014 | optional: true
2015 |
2016 | is-number@7.0.0:
2017 | optional: true
2018 |
2019 | jju@1.4.0: {}
2020 |
2021 | js-tokens@4.0.0: {}
2022 |
2023 | jsesc@3.1.0: {}
2024 |
2025 | json-schema-traverse@1.0.0: {}
2026 |
2027 | json5@2.2.3: {}
2028 |
2029 | jsonfile@6.2.0:
2030 | dependencies:
2031 | universalify: 2.0.1
2032 | optionalDependencies:
2033 | graceful-fs: 4.2.11
2034 |
2035 | kolorist@1.8.0: {}
2036 |
2037 | linkify-it@5.0.0:
2038 | dependencies:
2039 | uc.micro: 2.1.0
2040 |
2041 | local-pkg@1.1.2:
2042 | dependencies:
2043 | mlly: 1.7.4
2044 | pkg-types: 2.3.0
2045 | quansync: 0.2.11
2046 |
2047 | lodash@4.17.21: {}
2048 |
2049 | lru-cache@5.1.1:
2050 | dependencies:
2051 | yallist: 3.1.1
2052 |
2053 | lru-cache@6.0.0:
2054 | dependencies:
2055 | yallist: 4.0.0
2056 |
2057 | lunr@2.3.9: {}
2058 |
2059 | magic-string@0.30.17:
2060 | dependencies:
2061 | '@jridgewell/sourcemap-codec': 1.5.5
2062 |
2063 | markdown-it@14.1.0:
2064 | dependencies:
2065 | argparse: 2.0.1
2066 | entities: 4.5.0
2067 | linkify-it: 5.0.0
2068 | mdurl: 2.0.0
2069 | punycode.js: 2.3.1
2070 | uc.micro: 2.1.0
2071 |
2072 | mdurl@2.0.0: {}
2073 |
2074 | micromatch@4.0.8:
2075 | dependencies:
2076 | braces: 3.0.3
2077 | picomatch: 2.3.1
2078 | optional: true
2079 |
2080 | minimatch@10.0.3:
2081 | dependencies:
2082 | '@isaacs/brace-expansion': 5.0.0
2083 |
2084 | minimatch@9.0.5:
2085 | dependencies:
2086 | brace-expansion: 2.0.2
2087 |
2088 | mlly@1.7.4:
2089 | dependencies:
2090 | acorn: 8.15.0
2091 | pathe: 2.0.3
2092 | pkg-types: 1.3.1
2093 | ufo: 1.6.1
2094 |
2095 | ms@2.1.3: {}
2096 |
2097 | muggle-string@0.4.1: {}
2098 |
2099 | nanoid@3.3.11: {}
2100 |
2101 | node-addon-api@7.1.1:
2102 | optional: true
2103 |
2104 | node-releases@2.0.19: {}
2105 |
2106 | path-browserify@1.0.1: {}
2107 |
2108 | path-parse@1.0.7: {}
2109 |
2110 | pathe@2.0.3: {}
2111 |
2112 | picocolors@1.1.1: {}
2113 |
2114 | picomatch@2.3.1:
2115 | optional: true
2116 |
2117 | picomatch@4.0.3: {}
2118 |
2119 | pkg-types@1.3.1:
2120 | dependencies:
2121 | confbox: 0.1.8
2122 | mlly: 1.7.4
2123 | pathe: 2.0.3
2124 |
2125 | pkg-types@2.3.0:
2126 | dependencies:
2127 | confbox: 0.2.2
2128 | exsolve: 1.0.7
2129 | pathe: 2.0.3
2130 |
2131 | playwright-core@1.57.0: {}
2132 |
2133 | playwright@1.57.0:
2134 | dependencies:
2135 | playwright-core: 1.57.0
2136 | optionalDependencies:
2137 | fsevents: 2.3.2
2138 |
2139 | postcss@8.5.6:
2140 | dependencies:
2141 | nanoid: 3.3.11
2142 | picocolors: 1.1.1
2143 | source-map-js: 1.2.1
2144 |
2145 | punycode.js@2.3.1: {}
2146 |
2147 | punycode@2.3.1: {}
2148 |
2149 | quansync@0.2.11: {}
2150 |
2151 | react-dom@19.2.3(react@19.2.3):
2152 | dependencies:
2153 | react: 19.2.3
2154 | scheduler: 0.27.0
2155 |
2156 | react-refresh@0.18.0: {}
2157 |
2158 | react@19.2.3: {}
2159 |
2160 | readdirp@4.1.2:
2161 | optional: true
2162 |
2163 | require-from-string@2.0.2: {}
2164 |
2165 | resolve@1.22.10:
2166 | dependencies:
2167 | is-core-module: 2.16.1
2168 | path-parse: 1.0.7
2169 | supports-preserve-symlinks-flag: 1.0.0
2170 |
2171 | rollup@4.46.4:
2172 | dependencies:
2173 | '@types/estree': 1.0.8
2174 | optionalDependencies:
2175 | '@rollup/rollup-android-arm-eabi': 4.46.4
2176 | '@rollup/rollup-android-arm64': 4.46.4
2177 | '@rollup/rollup-darwin-arm64': 4.46.4
2178 | '@rollup/rollup-darwin-x64': 4.46.4
2179 | '@rollup/rollup-freebsd-arm64': 4.46.4
2180 | '@rollup/rollup-freebsd-x64': 4.46.4
2181 | '@rollup/rollup-linux-arm-gnueabihf': 4.46.4
2182 | '@rollup/rollup-linux-arm-musleabihf': 4.46.4
2183 | '@rollup/rollup-linux-arm64-gnu': 4.46.4
2184 | '@rollup/rollup-linux-arm64-musl': 4.46.4
2185 | '@rollup/rollup-linux-loongarch64-gnu': 4.46.4
2186 | '@rollup/rollup-linux-ppc64-gnu': 4.46.4
2187 | '@rollup/rollup-linux-riscv64-gnu': 4.46.4
2188 | '@rollup/rollup-linux-riscv64-musl': 4.46.4
2189 | '@rollup/rollup-linux-s390x-gnu': 4.46.4
2190 | '@rollup/rollup-linux-x64-gnu': 4.46.4
2191 | '@rollup/rollup-linux-x64-musl': 4.46.4
2192 | '@rollup/rollup-win32-arm64-msvc': 4.46.4
2193 | '@rollup/rollup-win32-ia32-msvc': 4.46.4
2194 | '@rollup/rollup-win32-x64-msvc': 4.46.4
2195 | fsevents: 2.3.3
2196 |
2197 | sass@1.83.0:
2198 | dependencies:
2199 | chokidar: 4.0.3
2200 | immutable: 5.1.3
2201 | source-map-js: 1.2.1
2202 | optionalDependencies:
2203 | '@parcel/watcher': 2.5.1
2204 | optional: true
2205 |
2206 | scheduler@0.27.0: {}
2207 |
2208 | semver@6.3.1: {}
2209 |
2210 | semver@7.5.4:
2211 | dependencies:
2212 | lru-cache: 6.0.0
2213 |
2214 | source-map-js@1.2.1: {}
2215 |
2216 | source-map-support@0.5.21:
2217 | dependencies:
2218 | buffer-from: 1.1.2
2219 | source-map: 0.6.1
2220 | optional: true
2221 |
2222 | source-map@0.6.1: {}
2223 |
2224 | sprintf-js@1.0.3: {}
2225 |
2226 | string-argv@0.3.2: {}
2227 |
2228 | strip-json-comments@3.1.1: {}
2229 |
2230 | supports-color@8.1.1:
2231 | dependencies:
2232 | has-flag: 4.0.0
2233 |
2234 | supports-preserve-symlinks-flag@1.0.0: {}
2235 |
2236 | terser@5.37.0:
2237 | dependencies:
2238 | '@jridgewell/source-map': 0.3.11
2239 | acorn: 8.15.0
2240 | commander: 2.20.3
2241 | source-map-support: 0.5.21
2242 | optional: true
2243 |
2244 | tinyglobby@0.2.15:
2245 | dependencies:
2246 | fdir: 6.5.0(picomatch@4.0.3)
2247 | picomatch: 4.0.3
2248 |
2249 | to-regex-range@5.0.1:
2250 | dependencies:
2251 | is-number: 7.0.0
2252 | optional: true
2253 |
2254 | typedoc@0.28.15(typescript@5.9.3):
2255 | dependencies:
2256 | '@gerrit0/mini-shiki': 3.17.0
2257 | lunr: 2.3.9
2258 | markdown-it: 14.1.0
2259 | minimatch: 9.0.5
2260 | typescript: 5.9.3
2261 | yaml: 2.8.1
2262 |
2263 | typescript@5.8.2: {}
2264 |
2265 | typescript@5.9.3: {}
2266 |
2267 | uc.micro@2.1.0: {}
2268 |
2269 | ufo@1.6.1: {}
2270 |
2271 | undici-types@7.16.0: {}
2272 |
2273 | universalify@2.0.1: {}
2274 |
2275 | update-browserslist-db@1.1.3(browserslist@4.25.3):
2276 | dependencies:
2277 | browserslist: 4.25.3
2278 | escalade: 3.2.0
2279 | picocolors: 1.1.1
2280 |
2281 | uri-js@4.4.1:
2282 | dependencies:
2283 | punycode: 2.3.1
2284 |
2285 | vite-plugin-dts@4.5.4(@types/node@24.9.1)(rollup@4.46.4)(typescript@5.9.3)(vite@7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1)):
2286 | dependencies:
2287 | '@microsoft/api-extractor': 7.52.11(@types/node@24.9.1)
2288 | '@rollup/pluginutils': 5.2.0(rollup@4.46.4)
2289 | '@volar/typescript': 2.4.23
2290 | '@vue/language-core': 2.2.0(typescript@5.9.3)
2291 | compare-versions: 6.1.1
2292 | debug: 4.4.1
2293 | kolorist: 1.8.0
2294 | local-pkg: 1.1.2
2295 | magic-string: 0.30.17
2296 | typescript: 5.9.3
2297 | optionalDependencies:
2298 | vite: 7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1)
2299 | transitivePeerDependencies:
2300 | - '@types/node'
2301 | - rollup
2302 | - supports-color
2303 |
2304 | vite@7.3.0(@types/node@24.9.1)(sass@1.83.0)(terser@5.37.0)(yaml@2.8.1):
2305 | dependencies:
2306 | esbuild: 0.27.1
2307 | fdir: 6.5.0(picomatch@4.0.3)
2308 | picomatch: 4.0.3
2309 | postcss: 8.5.6
2310 | rollup: 4.46.4
2311 | tinyglobby: 0.2.15
2312 | optionalDependencies:
2313 | '@types/node': 24.9.1
2314 | fsevents: 2.3.3
2315 | sass: 1.83.0
2316 | terser: 5.37.0
2317 | yaml: 2.8.1
2318 |
2319 | vscode-uri@3.1.0: {}
2320 |
2321 | yallist@3.1.1: {}
2322 |
2323 | yallist@4.0.0: {}
2324 |
2325 | yaml@2.8.1: {}
2326 |
2327 | zustand@5.0.9(@types/react@19.2.7)(react@19.2.3):
2328 | optionalDependencies:
2329 | '@types/react': 19.2.7
2330 | react: 19.2.3
2331 |
--------------------------------------------------------------------------------