diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3e3cab6..f18221e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -29,7 +29,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - run: pnpm run build - - uses: simenandre/publish-with-pnpm@v1 + + - name: Test + run: pnpm test + + - name: Build + run: | + pnpm run build + + - uses: simenandre/publish-with-pnpm@v2 with: npm-auth-token: ${{ secrets.NPM_ACCESS_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..928100c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + setup: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'pnpm' + + - uses: actions/cache@v4 + with: + path: "**/node_modules" + key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + lint: + needs: setup + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'pnpm' + + - name: Load node_modules + uses: actions/cache@v4 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Run linter for now-playing + run: pnpm format + + - name: Ensure there is no diff + run: | + git diff --exit-code + + test: + needs: setup + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'pnpm' + + - name: Load node_modules + uses: actions/cache@v4 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Run test for now-playing + run: pnpm test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5957216 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +.github +*.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fa51da2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/README.md b/README.md index 6eab858..c60f1e7 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,17 @@ yarn add @BolajiOlajide/now-playing ## Usage ```ts -import { NowPlaying, Providers } from "@BolajiOlajide/now-playing"; +import { NowPlaying, Providers } from '@BolajiOlajide/now-playing' const np = new NowPlaying(Providers.SPOTIFY, { useCache: false, // default is true cacheDuration: 30000, // in milliseconds streamerArgs: { - clientId: "foo", - clientSecret: "bar", - refreshToken: "baz", + clientId: 'foo', + clientSecret: 'bar', + refreshToken: 'baz', }, -}); +}) ``` ### Storage @@ -41,7 +41,7 @@ Data is stored in memory. This is to reduce overhead. We plan to expose the `ISt #### Spotify -You need two things. +You need three things. 1. Spotify Client ID 2. Spotify Client Secret diff --git a/examples/spotify.ts b/examples/spotify.ts index 46cb3cb..0b0090d 100644 --- a/examples/spotify.ts +++ b/examples/spotify.ts @@ -1,4 +1,4 @@ -import { NowPlaying, Providers } from "../dist"; +import { NowPlaying, Providers } from '../dist' const np = new NowPlaying(Providers.SPOTIFY, { streamerArgs: { @@ -6,7 +6,6 @@ const np = new NowPlaying(Providers.SPOTIFY, { clientSecret: 'bar', refreshToken: 'baz', }, -}); +}) - -console.log("Hello World", np); +console.log('Hello World', np) diff --git a/package.json b/package.json index d773fd5..e013f20 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "type": "module", "scripts": { "ex:spotify": "ts-node examples/spotify.ts", - "build": "npx rimraf dist && rollup --config" + "build": "npx rimraf dist && rollup --config", + "test": "vitest", + "format": "prettier . --write" }, "exports": { "types": "./dist/now-playing.d.ts", @@ -20,20 +22,23 @@ ], "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "dependencies": { - "@types/node-fetch": "2.6.11", "node-fetch": "2.6.7", "zod": "3.23.8" }, "devDependencies": { - "@rollup/plugin-typescript": "^11.1.6", - "@types/node": "^18.12.1", - "rollup": "^4.18.0", - "rollup-plugin-dts": "^6.1.1", + "@rollup/plugin-typescript": "11.1.6", + "@types/node": "20.14.9", + "@types/node-fetch": "2.6.11", + "msw": "2.3.1", + "prettier": "3.3.2", + "rollup": "4.18.0", + "rollup-plugin-dts": "6.1.1", "ts-node": "10.9.2", - "tslib": "^2.6.3", - "typescript": "5.4.5" + "tslib": "2.6.3", + "typescript": "5.4.5", + "vitest": "1.6.0" }, "engines": { "node": ">=18" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6aee3a..f45e66a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@types/node-fetch': - specifier: 2.6.11 - version: 2.6.11 node-fetch: specifier: 2.6.7 version: 2.6.7 @@ -17,26 +14,38 @@ dependencies: devDependencies: '@rollup/plugin-typescript': - specifier: ^11.1.6 + specifier: 11.1.6 version: 11.1.6(rollup@4.18.0)(tslib@2.6.3)(typescript@5.4.5) '@types/node': - specifier: ^18.12.1 - version: 18.19.34 + specifier: 20.14.9 + version: 20.14.9 + '@types/node-fetch': + specifier: 2.6.11 + version: 2.6.11 + msw: + specifier: 2.3.1 + version: 2.3.1(typescript@5.4.5) + prettier: + specifier: 3.3.2 + version: 3.3.2 rollup: - specifier: ^4.18.0 + specifier: 4.18.0 version: 4.18.0 rollup-plugin-dts: - specifier: ^6.1.1 + specifier: 6.1.1 version: 6.1.1(rollup@4.18.0)(typescript@5.4.5) ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@18.19.34)(typescript@5.4.5) + version: 10.9.2(@types/node@20.14.9)(typescript@5.4.5) tslib: - specifier: ^2.6.3 + specifier: 2.6.3 version: 2.6.3 typescript: specifier: 5.4.5 version: 5.4.5 + vitest: + specifier: 1.6.0 + version: 1.6.0(@types/node@20.14.9) packages: @@ -69,6 +78,18 @@ packages: dev: true optional: true + /@bundled-es-modules/cookie@2.0.0: + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + dependencies: + cookie: 0.5.0 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -76,6 +97,257 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@inquirer/confirm@3.1.10: + resolution: {integrity: sha512-/aAHu83Njy6yf44T+ZrRPUkMcUqprrOiIKsyMvf9jOV+vF5BNb2ja1aLP33MK36W8eaf91MTL/mU/e6METuENg==} + engines: {node: '>=18'} + dependencies: + '@inquirer/core': 8.2.3 + '@inquirer/type': 1.3.3 + dev: true + + /@inquirer/core@8.2.3: + resolution: {integrity: sha512-WrpDVPAaxJQjHid3Ra4FhUO70YBzkHSYVyW5X48L5zHYdudoPISJqTRRWSeamHfaXda7PNNaC5Py5MEo7QwBNA==} + engines: {node: '>=18'} + dependencies: + '@inquirer/figures': 1.0.3 + '@inquirer/type': 1.3.3 + '@types/mute-stream': 0.0.4 + '@types/node': 20.14.9 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /@inquirer/figures@1.0.3: + resolution: {integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==} + engines: {node: '>=18'} + dev: true + + /@inquirer/type@1.3.3: + resolution: {integrity: sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==} + engines: {node: '>=18'} + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -92,6 +364,38 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@mswjs/cookies@1.1.1: + resolution: {integrity: sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==} + engines: {node: '>=18'} + dev: true + + /@mswjs/interceptors@0.29.1: + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.2 + strict-event-emitter: 0.5.1 + dev: true + + /@open-draft/deferred-promise@2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + dev: true + + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + dev: true + /@rollup/plugin-typescript@11.1.6(rollup@4.18.0)(tslib@2.6.3)(typescript@5.4.5): resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} engines: {node: '>=14.0.0'} @@ -255,6 +559,10 @@ packages: dev: true optional: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@tsconfig/node10@1.0.11: resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} dev: true @@ -271,33 +579,105 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true + /@types/mute-stream@0.0.4: + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + dependencies: + '@types/node': 20.14.9 + dev: true + /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: - '@types/node': 18.19.34 + '@types/node': 20.14.9 form-data: 4.0.0 - dev: false + dev: true - /@types/node@18.19.34: - resolution: {integrity: sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==} + /@types/node@20.14.9: + resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==} dependencies: undici-types: 5.26.5 + dev: true + + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + dev: true + + /@vitest/expect@1.6.0: + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + dependencies: + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.6.0: + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + dependencies: + '@vitest/utils': 1.6.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.6.0: + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + dependencies: + magic-string: 0.30.10 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.6.0: + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.6.0: + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true - /acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + /acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.12.0 dev: true - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + /acorn@8.12.0: + resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -307,13 +687,47 @@ packages: dev: true optional: true + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -326,6 +740,39 @@ packages: dev: true optional: true + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} requiresBuild: true @@ -334,33 +781,126 @@ packages: dev: true optional: true + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} requiresBuild: true dev: true optional: true + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: false + dev: true + + /confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + dev: true + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: false + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} dev: true + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: true + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -372,6 +912,27 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -379,7 +940,7 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false + dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -393,6 +954,25 @@ packages: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + + /graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -400,6 +980,11 @@ packages: dev: true optional: true + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -407,18 +992,64 @@ packages: function-bind: 1.1.2 dev: true - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + /headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + + /is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} dependencies: hasown: 2.0.2 dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} requiresBuild: true dev: true optional: true + /js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + dev: true + + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.7.1 + pkg-types: 1.1.1 + dev: true + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} dependencies: @@ -429,17 +1060,81 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: false + dev: true /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: false + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mlly@1.7.1: + resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + dependencies: + acorn: 8.12.0 + pathe: 1.1.2 + pkg-types: 1.1.1 + ufo: 1.5.3 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /msw@2.3.1(typescript@5.4.5): + resolution: {integrity: sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.1.10 + '@mswjs/cookies': 1.1.1 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.20.1 + typescript: 5.4.5 + yargs: 17.7.2 + dev: true + + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true /node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} @@ -453,26 +1148,112 @@ packages: whatwg-url: 5.0.0 dev: false + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /outvariant@1.4.2: + resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} + dev: true + + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + dev: true + + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - requiresBuild: true dev: true - optional: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} dev: true + /pkg-types@1.1.1: + resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + dependencies: + confbox: 0.1.7 + mlly: 1.7.1 + pathe: 1.1.2 + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + + /prettier@3.3.2: + resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.13.1 + is-core-module: 2.14.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -517,6 +1298,76 @@ packages: fsevents: 2.3.3 dev: true + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + + /strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + dependencies: + js-tokens: 9.0.0 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -526,16 +1377,37 @@ packages: dev: true optional: true + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} dev: true + /tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + dev: true + + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false - /ts-node@10.9.2(@types/node@18.19.34)(typescript@5.4.5): + /ts-node@10.9.2(@types/node@20.14.9)(typescript@5.4.5): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -554,9 +1426,9 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 18.19.34 - acorn: 8.11.3 - acorn-walk: 8.3.2 + '@types/node': 20.14.9 + acorn: 8.12.0 + acorn-walk: 8.3.3 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -570,19 +1442,152 @@ packages: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} dev: true + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@4.20.1: + resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==} + engines: {node: '>=16'} + dev: true + /typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true dev: true + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true + /vite-node@1.6.0(@types/node@20.14.9): + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.5 + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.3.1(@types/node@20.14.9) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@5.3.1(@types/node@20.14.9): + resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.14.9 + esbuild: 0.21.5 + postcss: 8.4.38 + rollup: 4.18.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.6.0(@types/node@20.14.9): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.14.9 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.3 + chai: 4.4.1 + debug: 4.3.5 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.3.1(@types/node@20.14.9) + vite-node: 1.6.0(@types/node@20.14.9) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false @@ -594,11 +1599,74 @@ packages: webidl-conversions: 3.0.1 dev: false + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} dev: true + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false diff --git a/rollup.config.js b/rollup.config.js index 10a807e..33f1c33 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,34 +1,34 @@ -import typescript from "@rollup/plugin-typescript"; -import dts from "rollup-plugin-dts"; +import typescript from '@rollup/plugin-typescript' +import dts from 'rollup-plugin-dts' const config = [ { - input: "./src/index.ts", + input: './src/index.ts', output: [ { - file: "dist/now-playing.mjs", - format: "esm", + file: 'dist/now-playing.mjs', + format: 'esm', sourcemap: true, - name: "NowPlaying", + name: 'NowPlaying', }, { - file: "./dist/now-playing.js", - format: "umd", + file: './dist/now-playing.js', + format: 'umd', sourcemap: true, - name: "NowPlaying", + name: 'NowPlaying', }, ], - external: ["zod", "node-fetch"], + external: ['zod', 'node-fetch'], plugins: [typescript()], }, { - input: "./src/index.ts", + input: './src/index.ts', output: { - file: "dist/now-playing.d.ts", - format: "esm", + file: 'dist/now-playing.d.ts', + format: 'esm', }, plugins: [dts()], }, -]; +] -export default config; +export default config diff --git a/src/constants.ts b/src/constants.ts index 0578c08..17b2a7c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -export const SPOTIFY_ACCESS_TOKEN_KEY = "spotify_access_token"; -export const SPOTIFY_TRACK_KEY = "spotify_track"; +export const SPOTIFY_ACCESS_TOKEN_KEY = 'spotify_access_token' +export const SPOTIFY_TRACK_KEY = 'spotify_track' -export const CACHE_DURATION_MS = 60000; +export const CACHE_DURATION_MS = 60000 diff --git a/src/fixtures/spotify.fixture.ts b/src/fixtures/spotify.fixture.ts new file mode 100644 index 0000000..7eb64b0 --- /dev/null +++ b/src/fixtures/spotify.fixture.ts @@ -0,0 +1,45 @@ +import type { Song } from '../streamers' +import type { SpotifyTrack } from '../streamers/spotify.streamer' + +export const KanyeHomeComingSong: Song = { + is_playing: false, + title: 'Coming Home', + artiste: 'Kanye West', + image_url: 'https://i.scdn.co/image/ab67616d0000b273712549143', + preview_url: + 'https://p.scdn.co/mp3-preview/0663627c27885467868491a91185643b0508975c?cid=774b29d4f13844c495f206cafdad9c86', + url: 'https://open.spotify.com/track/6M2wZ9GZgrQXHCFfjv46we', +} + +export const KanyeHomeComingSongResponse: { track: SpotifyTrack } = { + track: { + name: 'Coming Home', + artists: [{ name: 'Kanye West' }], + external_urls: { + spotify: 'https://open.spotify.com/track/6M2wZ9GZgrQXHCFfjv46we', + }, + album: { + images: [{ url: 'https://i.scdn.co/image/ab67616d0000b273712549143' }], + }, + preview_url: + 'https://p.scdn.co/mp3-preview/0663627c27885467868491a91185643b0508975c?cid=774b29d4f13844c495f206cafdad9c86', + }, +} + +export const CurrentlyPlayingTrackResponse: SpotifyTrack = { + name: 'Kolwa', + artists: [{ name: 'Euggy' }, { name: 'Suraj' }, { name: 'Mumba Yachi' }], + external_urls: { + spotify: + 'https://open.spotify.com/track/4U6zIONOpmnby5fvOM6han?si=a7661613c3fa46c3', + }, + album: { + images: [ + { + url: 'https://i.scdn.co/image/ab67616d0000b273712549143', + }, + ], + }, + preview_url: + 'https://p.scdn.co/mp3-preview/0663627c27885467868491a91185643b0508975c?cid=774b29d4f13844c495f206cafdad9c86', +} diff --git a/src/index.ts b/src/index.ts index 66b723e..2e30f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export { NowPlaying } from './main' -export { Providers } from './schema' +export { Providers } from './schema' diff --git a/src/main.ts b/src/main.ts index d7adca4..f155da6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,12 @@ import { SpotifyStreamerArgs, } from './schema' import { InMemoryStorage, type IStorer } from './storage' -import { SpotifyStreamer, NoopStreamer, type IStreamer, Song } from './streamers' +import { + SpotifyStreamer, + NoopStreamer, + type IStreamer, + Song, +} from './streamers' import { ValidationError } from './error' // NowPlaying allows one to get the currently playing song for a streaming platform @@ -37,7 +42,7 @@ export class NowPlaying { this.parseArgs(args) this.streamerArgs = args.streamerArgs this.useCache = args.useCache || true - this.cacheDuration = args.cacheDuration || CACHE_DURATION_MS; + this.cacheDuration = args.cacheDuration || CACHE_DURATION_MS // We only support in memory storage for now, if there's a need we can // support more storage mechanism. @@ -72,10 +77,16 @@ export class NowPlaying { } private getStreamer(): IStreamer { - const cacheOpts = { useCache: this.useCache, cacheDuration: this.cacheDuration } + const cacheOpts = { + useCache: this.useCache, + cacheDuration: this.cacheDuration, + } switch (this.provider) { case Providers.SPOTIFY: - return new SpotifyStreamer(this.storer, { ...this.streamerArgs as SpotifyStreamerArgs, ...cacheOpts }) + return new SpotifyStreamer(this.storer, { + ...(this.streamerArgs as SpotifyStreamerArgs), + ...cacheOpts, + }) case Providers.NOOP: return new NoopStreamer() default: diff --git a/src/schema.ts b/src/schema.ts index 933bb7e..23bc8a1 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,42 +1,40 @@ -import * as z from "zod"; +import { z } from 'zod' export enum Providers { SPOTIFY = 'SPOTIFY', NOOP = 'NOOP', } -export const providerSchema = z.nativeEnum(Providers); +export const providerSchema = z.nativeEnum(Providers) export const BaseNowPlayingArgsSchema = z.object({ useCache: z.boolean().optional(), - cacheDuration: z - .number() - .optional() -}); -export type BaseNowPlayingArgs = z.infer; + cacheDuration: z.number().optional(), +}) +export type BaseNowPlayingArgs = z.infer export const SpotifyStreamerArgsSchema = z.object({ clientId: z.string({ - message: "Spotify client ID (clientId) is required", - required_error: "Spotify client ID (clientId) is required", + message: 'Spotify client ID (clientId) is required', + required_error: 'Spotify client ID (clientId) is required', }), clientSecret: z.string({ - message: "Spotify client secret (clientSecret) is required", - required_error: "Spotify client secret (clientSecret) is required", + message: 'Spotify client secret (clientSecret) is required', + required_error: 'Spotify client secret (clientSecret) is required', }), refreshToken: z.string({ - message: "Spotify refresh token (refreshToken) is required", - required_error: "Spotify refresh token (refreshToken) is required", + message: 'Spotify refresh token (refreshToken) is required', + required_error: 'Spotify refresh token (refreshToken) is required', }), -}); -export type SpotifyStreamerArgs = z.infer; +}) +export type SpotifyStreamerArgs = z.infer export const SpotifyProviderArgsSchema = BaseNowPlayingArgsSchema.extend({ streamerArgs: SpotifyStreamerArgsSchema, -}); -export type SpotifyProviderArgs = z.infer; +}) +export type SpotifyProviderArgs = z.infer export const NoopProviderArgsSchema = BaseNowPlayingArgsSchema.extend({ streamerArgs: z.never(), -}); -export type NoopProviderArgs = z.infer; +}) +export type NoopProviderArgs = z.infer diff --git a/src/storage/__tests__/inmemory.storage.spec.ts b/src/storage/__tests__/inmemory.storage.spec.ts new file mode 100644 index 0000000..24f66ff --- /dev/null +++ b/src/storage/__tests__/inmemory.storage.spec.ts @@ -0,0 +1,92 @@ +import { describe, test, beforeEach, expect } from 'vitest' + +import { InMemoryStorage } from '../inmemory.storage' + +describe('InMemoryStorage', () => { + let storage: InMemoryStorage + + beforeEach(() => { + storage = new InMemoryStorage() + }) + + test('should set and get a value', () => { + const key = 'testKey' + const value = 'testValue' + const duration = 1000 * 60 // 1 minute + + storage.set(key, value, duration) + const retrievedValue = storage.get(key) + + expect(retrievedValue).toBe(value) + }) + + test('should return undefined for an expired entry', () => { + const key = 'testKey' + const value = 'testValue' + const duration = -1000 // Expired + + storage.set(key, value, duration) + const retrievedValue = storage.get(key) + + expect(retrievedValue).toBeUndefined() + }) + + test('should delete an entry', () => { + const key = 'testKey' + const value = 'testValue' + const duration = 1000 * 60 // 1 minute + + storage.set(key, value, duration) + storage.delete(key) + const retrievedValue = storage.get(key) + + expect(retrievedValue).toBeUndefined() + }) + + test('should clear all entries', () => { + const key1 = 'testKey1' + const value1 = 'testValue1' + const key2 = 'testKey2' + const value2 = 'testValue2' + const duration = 1000 * 60 // 1 minute + + storage.set(key1, value1, duration) + storage.set(key2, value2, duration) + storage.clear() + + expect(storage.get(key1)).toBeUndefined() + expect(storage.get(key2)).toBeUndefined() + }) + + test('should return keys iterator', () => { + const key1 = 'testKey1' + const value1 = 'testValue1' + const key2 = 'testKey2' + const value2 = 'testValue2' + const duration = 1000 * 60 // 1 minute + + storage.set(key1, value1, duration) + storage.set(key2, value2, duration) + + const keys = Array.from(storage.keys()) + + expect(keys).toContain(key1) + expect(keys).toContain(key2) + }) + + test('should prune expired entries', () => { + const key1 = 'testKey1' + const value1 = 'testValue1' + const key2 = 'testKey2' + const value2 = 'testValue2' + const duration1 = 1000 * 60 // 1 minute + const duration2 = -1000 // Expired + + storage.set(key1, value1, duration1) + storage.set(key2, value2, duration2) + storage.pruneExpiredEntries() + + expect(storage.get(key1)).toBe(value1) + expect(storage.get(key2)).toBeUndefined() + }) +}) diff --git a/src/storage/inmemory.storage.ts b/src/storage/inmemory.storage.ts index 8ce2873..2a4326b 100644 --- a/src/storage/inmemory.storage.ts +++ b/src/storage/inmemory.storage.ts @@ -1,63 +1,63 @@ -import type { IStorer, CacheData } from "./types"; +import type { IStorer, CacheData } from './types' export class InMemoryStorage implements IStorer { - private data: Map> = new Map(); + private data: Map> = new Map() - set(key: string, value: T, duration: number): void { - const expiresAt = Date.now() + duration; + set(key: string, value: T, durationInMs: number): void { + const expiresAt = Date.now() + durationInMs this.data.set(key, { value, expiresAt, - }); + }) } get(key: string): T | undefined { - const entry = this.data.get(key); + const entry = this.data.get(key) if (entry && this.entryIsStillValid(entry)) { - return entry.value as T; + return entry.value as T } else { - this.data.delete(key); + this.data.delete(key) } - return undefined; + return undefined } has(key: string): boolean { - const entry = this.data.get(key); + const entry = this.data.get(key) if (entry && this.entryIsStillValid(entry)) { - return true; + return true } else { - this.data.delete(key); - return false; + this.data.delete(key) + return false } } delete(key: string): boolean { - return this.data.delete(key); + return this.data.delete(key) } clear(): void { - this.data.clear(); + this.data.clear() } keys(): IterableIterator { - return this.data.keys(); + return this.data.keys() } pruneExpiredEntries(): void { - const now = Date.now(); + const now = Date.now() for (const [key, entry] of this.data) { if (!this.entryIsStillValid(entry)) { - this.data.delete(key); + this.data.delete(key) } } } private entryIsStillValid(entry: CacheData): boolean { - return entry.expiresAt > Date.now(); + return entry.expiresAt > Date.now() } } diff --git a/src/storage/types.ts b/src/storage/types.ts index 7f1ac38..3b2df97 100644 --- a/src/storage/types.ts +++ b/src/storage/types.ts @@ -1,13 +1,13 @@ export interface IStorer { - set(key: string, value: T, duration: number): void; - get(key: string): T | undefined; - delete(key: string): boolean; - has(key: string): boolean; - clear(): void; - pruneExpiredEntries(): void; + set(key: string, value: T, duration: number): void + get(key: string): T | undefined + delete(key: string): boolean + has(key: string): boolean + clear(): void + pruneExpiredEntries(): void } export interface CacheData { - value: T; - expiresAt: number; + value: T + expiresAt: number } diff --git a/src/streamers/__tests__/spotify.streamer.spec.ts b/src/streamers/__tests__/spotify.streamer.spec.ts new file mode 100644 index 0000000..150c77f --- /dev/null +++ b/src/streamers/__tests__/spotify.streamer.spec.ts @@ -0,0 +1,402 @@ +import { + describe, + test, + expect, + vi, + beforeAll, + afterAll, + afterEach, + beforeEach, +} from 'vitest' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { type SpotifyAccessToken, SpotifyStreamer } from '../spotify.streamer' +import { InMemoryStorage } from '../../storage/inmemory.storage' +import { SPOTIFY_ACCESS_TOKEN_KEY, SPOTIFY_TRACK_KEY } from '../../constants' +import { + CurrentlyPlayingTrackResponse, + KanyeHomeComingSong, + KanyeHomeComingSongResponse, +} from '../../fixtures/spotify.fixture' + +const freshAccessToken = 'freshAccessToken' +const fetchAccessTokenHandler = http.post( + 'https://accounts.spotify.com/api/token', + async ({ request }) => { + const info = await request.formData() + if (info.get('refresh_token') !== 'test-refresh-token') { + return HttpResponse.json( + { + error: { + message: 'something went wrong', + status: 400, + }, + }, + { + status: 400, + statusText: 'Bad Request', + headers: { + 'content-type': 'application/json', + }, + } + ) + } + return HttpResponse.json( + { + access_token: freshAccessToken, + token_type: 'Bearer', + expires_in: 3600, + scope: 'user-read-email', + }, + { + status: 201, + statusText: 'Created', + headers: { + 'content-type': 'application/json', + }, + } + ) + } +) + +const accessTokenForError = 'fake-access-token' +const accessTokenForNull = 'freshAccessToken null' +const lastPlayedSongHandler = http.get( + 'https://api.spotify.com/v1/me/player/recently-played', + async ({ request }) => { + const authHeader = request.headers.get('Authorization') + if (authHeader === `Bearer ${accessTokenForError}`) { + return HttpResponse.json( + { + error: { + message: 'something went wrong', + status: 400, + }, + }, + { + status: 400, + statusText: 'Bad Request', + headers: { + 'content-type': 'application/json', + }, + } + ) + } else if (authHeader === `Bearer ${accessTokenForNull}`) { + return HttpResponse.json( + { + total: 0, + items: [], + }, + { + status: 200, + statusText: 'OK', + } + ) + } + + return HttpResponse.json( + { + total: 1, + items: [KanyeHomeComingSongResponse], + }, + { + status: 200, + statusText: 'OK', + } + ) + } +) + +const currPlayingaccessTokenForError = 'currPlayingaccessTokenForError' +const noCurrPlayingsongTOken = 'noCurrPlayingsongTOken' +const unauthenticatedToken = 'unauthenticatedToken' +const currentlyPlayingSongHandler = http.get( + 'https://api.spotify.com/v1/me/player/currently-playing', + async ({ request }) => { + const authHeader = request.headers.get('Authorization') + if (authHeader === `Bearer ${currPlayingaccessTokenForError}`) { + return HttpResponse.json( + { + error: { + message: 'something went wrong', + status: 400, + }, + }, + { + status: 400, + statusText: 'Bad Request', + } + ) + } else if (authHeader === `Bearer ${noCurrPlayingsongTOken}`) { + return HttpResponse.json({ + is_playing: false, + }) + } else if (authHeader === `Bearer ${unauthenticatedToken}`) { + return HttpResponse.json( + { + error: { + status: 401, + message: 'The access token expired', + }, + }, + { + status: 401, + statusText: 'Unauthorized', + } + ) + } + return HttpResponse.json({ + is_playing: true, + currently_playing_type: 'track', + item: CurrentlyPlayingTrackResponse, + }) + } +) + +describe('SpotifyStreamer', () => { + const storer = new InMemoryStorage() + const clientId = 'test-client-id' + const clientSecret = 'test-client-secret' + const refreshToken = 'test-refresh-token' + const useCache = true + const cacheDuration = 3600 + + const server = setupServer( + fetchAccessTokenHandler, + lastPlayedSongHandler, + currentlyPlayingSongHandler + ) + beforeAll(() => { + // Start the interception. + server.listen({ + // This tells MSW to throw an error whenever it + // encounters a request that doesn't have a + // matching request handler. + onUnhandledRequest: 'error', + }) + }) + + afterEach(() => { + // Remove any handlers you may have added + // in individual tests (runtime handlers). + server.resetHandlers() + }) + + afterAll(() => { + // Disable request interception and clean up. + server.close() + }) + + describe('getAccessToken()', () => { + const streamer = new SpotifyStreamer(storer, { + clientId, + clientSecret, + refreshToken, + useCache, + cacheDuration, + }) + + beforeEach(() => { + storer.clear() + }) + + test('fetch from cache if available', async () => { + const fakeAccessToken = "bolaji's fake access token" + const payload = { access_token: fakeAccessToken } + storer.set(SPOTIFY_ACCESS_TOKEN_KEY, payload, 5000) + const response = await streamer.getAccessToken(refreshToken) + expect(response.access_token).toBe(fakeAccessToken) + }) + + test('fetch from spotify if not available in cache (and save in cache afterwards', async () => { + const response = await streamer.getAccessToken(refreshToken) + expect(response.access_token).toBe(freshAccessToken) + + const storedToken = storer.get( + SPOTIFY_ACCESS_TOKEN_KEY + ) + expect(storedToken).toEqual(response) + }) + + test('throw error if token fetching is unsuccessful', async () => { + expect.assertions(1) + return streamer.getAccessToken('fake-refresh-token').catch((error) => { + expect(error).toEqual({ + message: 'something went wrong', + status: 400, + }) + }) + }) + }) + + describe('fetchLastPlayed()', () => { + const streamer = new SpotifyStreamer(storer, { + clientId, + clientSecret, + refreshToken, + useCache, + cacheDuration, + }) + + beforeEach(() => { + streamer.setUseCache(true) + storer.clear() + }) + + beforeAll(() => { + const fakeAccessToken = "bolaji's fake access token" + const payload = { access_token: fakeAccessToken } + storer.set(SPOTIFY_ACCESS_TOKEN_KEY, payload, 5000) + }) + afterAll(() => { + streamer.setUseCache(true) + }) + + test('should return an error if http request errors', async () => { + expect.assertions(1) + return streamer + .fetchLastPlayed({ + access_token: accessTokenForError, + } as SpotifyAccessToken) + .catch((error) => { + expect(error).toEqual({ + message: 'something went wrong', + status: 400, + }) + }) + }) + + test('should return null if endpoint doesnt contain item', async () => { + const response = await streamer.fetchLastPlayed({ + access_token: accessTokenForNull, + } as SpotifyAccessToken) + expect(response).toBeNull() + }) + + test('should return the last played song and not cache if useCache is false', async () => { + streamer.setUseCache(false) + const response = await streamer.fetchLastPlayed({ + access_token: freshAccessToken, + } as SpotifyAccessToken) + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toBeUndefined() + expect(response).toEqual(KanyeHomeComingSong) + }) + + test('should return the last played song and cache if useCache is true', async () => { + const response = await streamer.fetchLastPlayed({ + access_token: freshAccessToken, + } as SpotifyAccessToken) + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toEqual(response) + expect(response).toEqual(KanyeHomeComingSong) + }) + }) + + describe('fetchCurrentlyPlaying', () => { + const streamer = new SpotifyStreamer(storer, { + clientId, + clientSecret, + refreshToken, + useCache, + cacheDuration, + }) + + beforeEach(() => { + streamer.setUseCache(true) + storer.clear() + }) + + beforeAll(() => { + const fakeAccessToken = "bolaji's fake access token" + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: fakeAccessToken }, + 5000 + ) + }) + afterAll(() => { + streamer.setUseCache(true) + }) + + const songResult = { + title: 'Kolwa', + artiste: 'Euggy, Suraj, Mumba Yachi', + image_url: 'https://i.scdn.co/image/ab67616d0000b273712549143', + is_playing: true, + preview_url: + 'https://p.scdn.co/mp3-preview/0663627c27885467868491a91185643b0508975c?cid=774b29d4f13844c495f206cafdad9c86', + url: 'https://open.spotify.com/track/4U6zIONOpmnby5fvOM6han?si=a7661613c3fa46c3', + } + test('should fetch track from cache if it exists', async () => { + storer.set(SPOTIFY_TRACK_KEY, songResult, 5000) + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(songResult) + }) + + test('should fetch new token and make request again if endpoint returns 401', async () => { + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: unauthenticatedToken, expires_in: 3000 }, + 5000 + ) + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(songResult) + + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toEqual(response) + }) + + test('should not save in cache if useCache is false', async () => { + streamer.setUseCache(false) + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: unauthenticatedToken, expires_in: 3000 }, + 5000 + ) + + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(songResult) + + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toBeUndefined() + }) + + test('should fetch last played song if response is not ok', async () => { + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: currPlayingaccessTokenForError, expires_in: 3000 }, + 5000 + ) + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(KanyeHomeComingSong) + + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toEqual(KanyeHomeComingSong) + }) + + test('should fetch last played song if no track is currently playing', async () => { + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: noCurrPlayingsongTOken, expires_in: 3000 }, + 5000 + ) + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(KanyeHomeComingSong) + + const cachedSong = storer.get(SPOTIFY_TRACK_KEY) + expect(cachedSong).toEqual(KanyeHomeComingSong) + }) + + test('should return currently playing song', async () => { + storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + { access_token: freshAccessToken, expires_in: 3000 }, + 5000 + ) + const response = await streamer.fetchCurrentlyPlaying() + expect(response).toEqual(songResult) + }) + }) +}) diff --git a/src/streamers/spotify.streamer.ts b/src/streamers/spotify.streamer.ts index f5c2630..fe7fc20 100644 --- a/src/streamers/spotify.streamer.ts +++ b/src/streamers/spotify.streamer.ts @@ -1,146 +1,200 @@ -import fetch, { Response } from 'node-fetch'; - -import type { Song, IStreamer, IStreamerCacheOpts } from './types'; -import { SpotifyStreamerArgsSchema, type SpotifyStreamerArgs } from '../schema'; -import type { IStorer } from '../storage'; -import { SPOTIFY_ACCESS_TOKEN_KEY, SPOTIFY_TRACK_KEY } from '../constants'; - -interface SpotifyAccessToken { - access_token: string; - token_type: string; - expires_in: number; - scope: string; - created_at: number; +import fetch, { Response } from 'node-fetch' + +import type { Song, IStreamer, IStreamerCacheOpts } from './types' +import { SpotifyStreamerArgsSchema, type SpotifyStreamerArgs } from '../schema' +import type { IStorer } from '../storage' +import { SPOTIFY_ACCESS_TOKEN_KEY, SPOTIFY_TRACK_KEY } from '../constants' + +export interface SpotifyAccessToken { + access_token: string + token_type: string + expires_in: number + scope: string } -type SpotifyTrack = { - name: string; - artists: Array<{ name: string }>; - external_urls: { spotify: string }; - album: { images: Array<{ url: string }> }; - preview_url: string; +interface SpotifyErrorResponse { + error: SpotifyError +} + +interface SpotifyError { + status: number + message: string +} + +export interface SpotifyTrack { + name: string + artists: Array<{ name: string }> + external_urls: { spotify: string } + album: { images: Array<{ url: string }> } + preview_url: string } type SpotifyCurrrentlyPlayingResponse = { - is_playing: boolean; - currently_playing_type: string; - item: SpotifyTrack; + is_playing: boolean + currently_playing_type: string + item: SpotifyTrack } type SpotifyRecentlyPlayedResponse = { - total: number; - items: Array<{ track: SpotifyTrack }>; + total: number + items: Array<{ track: SpotifyTrack }> } export class SpotifyStreamer implements IStreamer { - private clientId: string; - private clientSecret: string; - private refreshToken: string; - private storer: IStorer; - private useCache: boolean; - private cacheDuration: number; + private clientId: string + private clientSecret: string + private refreshToken: string + private storer: IStorer + private useCache: boolean + private cacheDuration: number constructor(storer: IStorer, args: SpotifyStreamerArgs & IStreamerCacheOpts) { - this.storer = storer; - SpotifyStreamerArgsSchema.parse(args); - - this.clientId = args.clientId; - this.clientSecret = args.clientSecret; - this.refreshToken = args.refreshToken; - this.useCache = args.useCache; - this.cacheDuration = args.cacheDuration; + this.storer = storer + SpotifyStreamerArgsSchema.parse(args) + + this.clientId = args.clientId + this.clientSecret = args.clientSecret + this.refreshToken = args.refreshToken + this.useCache = args.useCache + this.cacheDuration = args.cacheDuration } - public async getAccessToken(refreshToken: string, forceRefresh: boolean = false): Promise { - const existingAccessToken = this.storer.get(SPOTIFY_ACCESS_TOKEN_KEY); + public async getAccessToken( + refreshToken: string, + forceRefresh: boolean = false + ): Promise { + const existingAccessToken = this.storer.get( + SPOTIFY_ACCESS_TOKEN_KEY + ) - if (!forceRefresh && existingAccessToken && (existingAccessToken.created_at + existingAccessToken.expires_in > Date.now())) { - return existingAccessToken; + if (!forceRefresh && existingAccessToken) { + return existingAccessToken } - const tempToken = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); - const headers = { Authorization: `Basic ${tempToken}`, 'Content-Type': 'application/x-www-form-urlencoded' }; - - const params = new URLSearchParams(); - params.append('grant_type', 'refresh_token'); - params.append('refresh_token', refreshToken); + const tempToken = Buffer.from( + `${this.clientId}:${this.clientSecret}` + ).toString('base64') + const headers = { + Authorization: `Basic ${tempToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + } - const response: Response = await fetch('https://accounts.spotify.com/api/token', { method: 'POST', headers, body: params }); - const jsonData = await response.json() as SpotifyAccessToken; - this.storer.set(SPOTIFY_ACCESS_TOKEN_KEY, jsonData, jsonData.expires_in - 1000); - return jsonData; + const params = new URLSearchParams() + params.append('grant_type', 'refresh_token') + params.append('refresh_token', refreshToken) + + const response: Response = await fetch( + 'https://accounts.spotify.com/api/token', + { method: 'POST', headers, body: params } + ) + if (!response.ok) { + const errorInfo = (await response.json()) as SpotifyErrorResponse + return Promise.reject(errorInfo.error) + } + const jsonData = (await response.json()) as SpotifyAccessToken + this.storer.set( + SPOTIFY_ACCESS_TOKEN_KEY, + jsonData, + jsonData.expires_in - 1000 + ) + return jsonData } public async fetchCurrentlyPlaying(): Promise { if (this.useCache) { - const cachedSong = this.storer.get(SPOTIFY_TRACK_KEY); + const cachedSong = this.storer.get(SPOTIFY_TRACK_KEY) if (cachedSong) { - return cachedSong; + return cachedSong } } - let accessToken = await this.getAccessToken(this.refreshToken); + let accessToken = await this.getAccessToken(this.refreshToken) - const headers = { Authorization: `Bearer ${accessToken.access_token}`, 'Content-Type': 'application/json', Accept: 'application/json' }; - let response: Response = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { method: 'GET', headers }); + const headers = { + Authorization: `Bearer ${accessToken.access_token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + } + let response: Response = await fetch( + 'https://api.spotify.com/v1/me/player/currently-playing', + { method: 'GET', headers } + ) if (response.status === 401) { - accessToken = await this.getAccessToken(this.refreshToken, true); - headers.Authorization = `Bearer ${accessToken.access_token}`; - response = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { method: 'GET', headers }); + accessToken = await this.getAccessToken(this.refreshToken, true) + headers.Authorization = `Bearer ${accessToken.access_token}` + response = await fetch( + 'https://api.spotify.com/v1/me/player/currently-playing', + { method: 'GET', headers } + ) } if (response.status === 204 || !response.ok) { - return this.fetchLastPlayed(accessToken); + return this.fetchLastPlayed(accessToken) } - const jsonData = await response.json() as SpotifyCurrrentlyPlayingResponse; + const jsonData = (await response.json()) as SpotifyCurrrentlyPlayingResponse if (!jsonData.is_playing || jsonData.currently_playing_type !== 'track') { - return this.fetchLastPlayed(accessToken); + return this.fetchLastPlayed(accessToken) } - const track = jsonData.item; + const track = jsonData.item const song: Song = { is_playing: jsonData.is_playing, title: track.name, - artiste: track.artists.map(artist => artist.name).join(', '), + artiste: track.artists.map((artist) => artist.name).join(', '), image_url: track.album.images[0].url, preview_url: track.preview_url, url: track.external_urls.spotify, - }; + } if (this.useCache) { - this.storer.set(SPOTIFY_TRACK_KEY, song, this.cacheDuration); + this.storer.set(SPOTIFY_TRACK_KEY, song, this.cacheDuration) } - return song; + return song } - private async fetchLastPlayed(accessToken: SpotifyAccessToken): Promise { - const headers = { Authorization: `Bearer ${accessToken.access_token}`, 'Content-Type': 'application/json', Accept: 'application/json' }; - const response: Response = await fetch('https://api.spotify.com/v1/me/player/recently-played?limit=1', { method: 'GET', headers }); - const jsonData = await response.json() as SpotifyRecentlyPlayedResponse; + async fetchLastPlayed(accessToken: SpotifyAccessToken): Promise { + const headers = { + Authorization: `Bearer ${accessToken.access_token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + } + const response: Response = await fetch( + 'https://api.spotify.com/v1/me/player/recently-played?limit=1', + { method: 'GET', headers } + ) + if (!response.ok) { + const errorInfo = (await response.json()) as SpotifyErrorResponse + return Promise.reject(errorInfo.error) + } + const jsonData = (await response.json()) as SpotifyRecentlyPlayedResponse - if (jsonData.items.length === 0) { - return null; + if (jsonData.items.length === 0 || jsonData.total === 0) { + return null } - const [{ track: lastPlayedTrack }] = jsonData.items; + const [{ track: lastPlayedTrack }] = jsonData.items const lastPlayed = { is_playing: false, title: lastPlayedTrack.name, - artiste: lastPlayedTrack.artists.map(artist => artist.name).join(', '), + artiste: lastPlayedTrack.artists.map((artist) => artist.name).join(', '), image_url: lastPlayedTrack.album.images[0].url, preview_url: lastPlayedTrack.preview_url, url: lastPlayedTrack.external_urls.spotify, - }; + } if (this.useCache) { - this.storer.set(SPOTIFY_TRACK_KEY, lastPlayed, this.cacheDuration); + this.storer.set(SPOTIFY_TRACK_KEY, lastPlayed, this.cacheDuration) } - return lastPlayed; + return lastPlayed + } + + setUseCache(useCache: boolean): void { + this.useCache = useCache } } diff --git a/src/streamers/types.ts b/src/streamers/types.ts index c9edcac..a03fb97 100644 --- a/src/streamers/types.ts +++ b/src/streamers/types.ts @@ -1,17 +1,17 @@ export interface IStreamer { - fetchCurrentlyPlaying(): Promise; + fetchCurrentlyPlaying(): Promise } export interface IStreamerCacheOpts { - useCache: boolean; - cacheDuration: number; + useCache: boolean + cacheDuration: number } export interface Song { - title: string; - artiste: string; - image_url: string; - is_playing: boolean; - preview_url: string; - url: string; + title: string + artiste: string + image_url: string + is_playing: boolean + preview_url: string + url: string } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2d80c8f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: '@BolajiOlajide/now-playing', + dir: './src', + open: false, + bail: 3, + }, +})