diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b043cd3ad..b3a5639ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ You should also include the user name that made the change. - アクティブユーザー数チャートの記録上限値を拡張 - Playのソースコード上限文字数を2倍に拡張 - 付箋ウィジェットの高さを設定可能に +- oEmbedをサポートしているウェブサイトのプレビューができるように + - YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように + - ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります) + - (すでにブラウザーでキャッシュされたリンクに対しては以前のプレビュー行動が行われてます。その場合、ブラウザーのキャッシュをクリアしてまた試してください。) - 配送先サーバーが410 Goneで応答してきた場合は自動で配送停止をするように - avatarBlurHash/bannerBlurHashの型をstringに限定 - APオブジェクトを入力してフェッチする機能とユーザーやノートの検索機能を分離 diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 2ce7293a52..21cf414087 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -19,9 +19,6 @@ export class UrlPreviewService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - private metaService: MetaService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, @@ -51,15 +48,15 @@ export class UrlPreviewService { reply.code(400); return; } - + const lang = request.query.lang; if (Array.isArray(lang)) { reply.code(400); return; } - + const meta = await this.metaService.fetch(); - + this.logger.info(meta.summalyProxy ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); @@ -85,16 +82,16 @@ export class UrlPreviewService { throw new Error('unsupported schema included'); } - if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { + if (summary.player.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { throw new Error('unsupported schema included'); } - + summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); - + // Cache 7days reply.header('Cache-Control', 'max-age=604800, immutable'); - + return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 594f8781bd..54404c8c53 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -97,7 +97,9 @@ "eslint-plugin-vue": "9.9.0", "happy-dom": "8.9.0", "start-server-and-test": "2.0.0", + "summaly": "github:misskey-dev/summaly", "vitest": "^0.29.2", + "vitest-fetch-mock": "^0.2.2", "vue-eslint-parser": "9.1.0", "vue-tsc": "1.2.0" } diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 5381ecbfa5..094709e093 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -1,7 +1,18 @@ <template> -<template v-if="playerEnabled"> - <div :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> +<template v-if="player.url && playerEnabled"> + <div + :class="$style.player" + :style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`" + > + <iframe + v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" + sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" + scrolling="no" + :allow="player.allow.join(';')" + :class="$style.playerIframe" + :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" + :style="{ border: 0 }" + ></iframe> <span v-else>invalid url</span> </div> <div :class="$style.action"> @@ -28,7 +39,7 @@ <header :class="$style.header"> <h1 v-if="unknownUrl" :class="$style.title">{{ url }}</h1> <h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1> - <h1 v-else :class="$style.title" :title="title">{{ title }}</h1> + <h1 v-else :class="$style.title" :title="title ?? undefined">{{ title }}</h1> </header> <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p> <p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p> @@ -37,7 +48,7 @@ <img v-if="icon" :class="$style.siteIcon" :src="icon"/> <p v-if="unknownUrl" :class="$style.siteName">?</p> <p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p> - <p v-else :class="$style.siteName" :title="sitename">{{ sitename }}</p> + <p v-else :class="$style.siteName" :title="sitename ?? undefined">{{ sitename }}</p> </footer> </article> </component> @@ -59,6 +70,7 @@ <script lang="ts" setup> import { defineAsyncComponent, onUnmounted } from 'vue'; +import type { summaly } from 'summaly'; import { url as local } from '@/config'; import { i18n } from '@/i18n'; import * as os from '@/os'; @@ -66,6 +78,8 @@ import { deviceKind } from '@/scripts/device-kind'; import MkButton from '@/components/MkButton.vue'; import { versatileLang } from '@/scripts/intl-const'; +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + const props = withDefaults(defineProps<{ url: string; detail?: boolean; @@ -91,7 +105,7 @@ let player = $ref({ url: null, width: null, height: null, -}); +} as SummalyResult['player']); let playerEnabled = $ref(false); let tweetId = $ref<string | null>(null); let tweetExpanded = $ref(props.detail); @@ -114,11 +128,7 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => { - res.json().then(info => { - if (info.url == null) { - unknownUrl = true; - return; - } + res.json().then((info: SummalyResult) => { title = info.title; description = info.description; thumbnail = info.thumbnail; diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 96730e7b56..295107e143 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -1,4 +1,8 @@ import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); // Set i18n import locales from '../../../locales'; diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts new file mode 100644 index 0000000000..205982a40a --- /dev/null +++ b/packages/frontend/test/url-preview.test.ts @@ -0,0 +1,140 @@ +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import './init'; +import type { summaly } from 'summaly'; +import { directives } from '@/directives'; +import MkUrlPreview from '@/components/MkUrlPreview.vue'; + +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + +describe('MkMediaImage', () => { + const renderPreviewBy = async (summary: Partial<SummalyResult>): Promise<RenderResult> => { + if (!summary.player) { + summary.player = { + url: null, + width: null, + height: null, + allow: [], + }; + } + + fetchMock.mockOnceIf(/^\/url?/, () => { + return { + status: 200, + body: JSON.stringify(summary), + }; + }); + + const result = render(MkUrlPreview, { + props: { url: summary.url }, + global: { directives }, + }); + + await new Promise<void>(resolve => { + const observer = new MutationObserver(() => { + resolve(); + observer.disconnect(); + }); + observer.observe(result.container, { childList: true, subtree: true }); + }); + + return result; + }; + + const renderAndOpenPreview = async (summary: Partial<SummalyResult>): Promise<HTMLIFrameElement | null> => { + const mkUrlPreview = await renderPreviewBy(summary); + const buttons = mkUrlPreview.getAllByRole('button'); + buttons[0].click(); + // Wait for the click event to be fired + await Promise.resolve(); + + return mkUrlPreview.container.querySelector('iframe'); + }; + + afterEach(() => { + fetchMock.resetMocks(); + cleanup(); + }); + + test('Should render the description', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + description: 'Mocked description', + }); + mkUrlPreview.getByText('Mocked description'); + }); + + test('Having a player should render a button', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + const buttons = mkUrlPreview.getAllByRole('button'); + assert.strictEqual(buttons.length, 2, 'two buttons'); + }); + + test('Having a player should setup the iframe', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1'); + assert.strictEqual( + iframe?.sandbox.toString(), + 'allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin', + ); + }); + + test('Having a player with `allow` field should set permissions', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: ['fullscreen', 'web-share'], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.allow, 'fullscreen;web-share'); + }); + + test('Having a player width should keep the fixed aspect ratio', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: 400, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '50%'); + }); + + test('Having a player width should keep the fixed height', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '200px'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb2ed93060..f56f77dc97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -462,6 +462,7 @@ importers: seedrandom: 3.0.5 start-server-and-test: 2.0.0 strict-event-emitter-types: 2.0.0 + summaly: github:misskey-dev/summaly syuilo-password-strength: 0.0.1 textarea-caret: 3.1.0 three: 0.150.1 @@ -475,6 +476,7 @@ importers: vanilla-tilt: 1.8.0 vite: 4.1.4 vitest: ^0.29.2 + vitest-fetch-mock: ^0.2.2 vue: 3.2.47 vue-eslint-parser: 9.1.0 vue-plyr: 7.0.0 @@ -567,7 +569,9 @@ importers: eslint-plugin-vue: 9.9.0_eslint@8.35.0 happy-dom: 8.9.0 start-server-and-test: 2.0.0 + summaly: github.com/misskey-dev/summaly/1bab7afee616429b8bbf7a7cbcbb8ebcef66d992 vitest: 0.29.2_zcjcryjt4bqcdu7ggonulipgea + vitest-fetch-mock: 0.2.2_vitest@0.29.2 vue-eslint-parser: 9.1.0_eslint@8.35.0 vue-tsc: 1.2.0_typescript@4.9.5 @@ -2134,7 +2138,6 @@ packages: /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} - dev: false /@sinonjs/commons/2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} @@ -2316,7 +2319,6 @@ packages: engines: {node: '>=14.16'} dependencies: defer-to-connect: 2.0.1 - dev: false /@tabler/icons-webfont/2.10.0: resolution: {integrity: sha512-5WvGhztlM3la7NWf8Y6ktT+KD7zb/Hz/zdMeFjExXvEFupGvuANEnbGo1wXI4ADdSWUaRDtnQHcSGIjZ+gZ+OQ==} @@ -2638,7 +2640,6 @@ packages: /@types/http-cache-semantics/4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} - dev: false /@types/ioredis/4.28.10: resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==} @@ -3901,7 +3902,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 transitivePeerDependencies: - debug dev: false @@ -3909,7 +3910,7 @@ packages: /axios/0.27.2_debug@4.3.4: resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 form-data: 4.0.0 transitivePeerDependencies: - debug @@ -4348,7 +4349,6 @@ packages: /cacheable-lookup/7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} - dev: false /cacheable-request/10.2.8: resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==} @@ -4361,7 +4361,6 @@ packages: mimic-response: 4.0.0 normalize-url: 8.0.0 responselike: 3.0.0 - dev: false /cacheable-request/7.0.2: resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} @@ -4568,7 +4567,6 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.0.1 - dev: false /cheerio/1.0.0-rc.12: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} @@ -4581,7 +4579,6 @@ packages: htmlparser2: 8.0.1 parse5: 7.1.2 parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -5016,6 +5013,14 @@ packages: cross-spawn: 7.0.3 dev: true + /cross-fetch/3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: true + /cross-spawn/5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -5044,12 +5049,10 @@ packages: domhandler: 5.0.3 domutils: 3.0.1 nth-check: 2.1.1 - dev: false /css-what/6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - dev: false /css.escape/1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -5325,7 +5328,6 @@ packages: engines: {node: '>=10'} dependencies: mimic-response: 3.1.0 - dev: false /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -5398,7 +5400,6 @@ packages: /defer-to-connect/2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} - dev: false /define-properties/1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} @@ -6028,7 +6029,6 @@ packages: /escape-regexp/0.0.1: resolution: {integrity: sha512-jVgdsYRa7RKxTT6MKNC3gdT+BF0Gfhpel19+HMRZJC2L0PufB0XOBuXBoXj29NKHwuktnAXd1Z1lyiH/8vOTpw==} - dev: false /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -6952,7 +6952,7 @@ packages: readable-stream: 2.3.7 dev: false - /follow-redirects/1.15.2: + /follow-redirects/1.15.2_debug@4.3.4: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} peerDependencies: @@ -6960,6 +6960,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.4 /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -6992,7 +6994,6 @@ packages: /form-data-encoder/2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - dev: false /form-data/2.3.3: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} @@ -7434,7 +7435,6 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 3.0.0 - dev: false /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -7686,7 +7686,6 @@ packages: /html-entities/2.3.2: resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==} - dev: false /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -7702,7 +7701,6 @@ packages: /http-cache-semantics/4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: false /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} @@ -7758,7 +7756,6 @@ packages: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - dev: false /http_ece/1.1.0: resolution: {integrity: sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==} @@ -7976,7 +7973,6 @@ packages: /ip-regex/4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} engines: {node: '>=8'} - dev: false /ip-regex/5.0.0: resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} @@ -7995,7 +7991,6 @@ packages: /ipaddr.js/2.0.1: resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} engines: {node: '>= 10'} - dev: false /is-absolute-url/2.1.0: resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} @@ -8206,7 +8201,6 @@ packages: engines: {node: '>=8'} dependencies: ip-regex: 4.3.0 - dev: false /is-lambda/1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} @@ -9021,7 +9015,6 @@ packages: /jschardet/3.0.0: resolution: {integrity: sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==} engines: {node: '>=0.1.90'} - dev: false /jsdom/21.1.0: resolution: {integrity: sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==} @@ -9072,7 +9065,6 @@ packages: /json-buffer/3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: false /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -9200,7 +9192,6 @@ packages: resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} dependencies: json-buffer: 3.0.1 - dev: false /kind-of/3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} @@ -9467,7 +9458,6 @@ packages: /lowercase-keys/3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -9663,12 +9653,10 @@ packages: /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - dev: false /mimic-response/4.0.0: resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -9967,7 +9955,6 @@ packages: /netmask/2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - dev: false /next-tick/1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -10114,7 +10101,6 @@ packages: /normalize-url/8.0.0: resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} engines: {node: '>=14.16'} - dev: false /now-and-later/2.0.1: resolution: {integrity: sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==} @@ -10394,7 +10380,6 @@ packages: /p-cancelable/3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} - dev: false /p-finally/1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} @@ -10531,7 +10516,6 @@ packages: dependencies: domhandler: 5.0.3 parse5: 7.1.2 - dev: false /parse5/5.1.1: resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} @@ -11139,7 +11123,6 @@ packages: ipaddr.js: 2.0.1 is-ip: 3.1.0 netmask: 2.0.2 - dev: false /private-ip/3.0.0: resolution: {integrity: sha512-HkMBs4nMtrP+cvcw0bDi2BAZIGgiKI4Zq8Oc+dMqNBpHS8iGL4+WO/pRtc8Bwnv9rjnV0QwMDwEBymFtqv7Kww==} @@ -11440,7 +11423,6 @@ packages: /quick-lru/5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - dev: false /random-seed/0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} @@ -11784,7 +11766,6 @@ packages: /resolve-alpn/1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: false /resolve-cwd/3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} @@ -11845,7 +11826,6 @@ packages: engines: {node: '>=14.16'} dependencies: lowercase-keys: 3.0.0 - dev: false /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -12994,7 +12974,6 @@ packages: /trace-redirect/1.0.6: resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==} - dev: false /traverse/0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -13588,6 +13567,18 @@ packages: optionalDependencies: fsevents: 2.3.2 + /vitest-fetch-mock/0.2.2_vitest@0.29.2: + resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} + engines: {node: '>=14.14.0'} + peerDependencies: + vitest: '>=0.16.0' + dependencies: + cross-fetch: 3.1.5 + vitest: 0.29.2_zcjcryjt4bqcdu7ggonulipgea + transitivePeerDependencies: + - encoding + dev: true + /vitest/0.29.2_zcjcryjt4bqcdu7ggonulipgea: resolution: {integrity: sha512-ydK9IGbAvoY8wkg29DQ4ivcVviCaUi3ivuPKfZEVddMTenFHUfB8EEDXQV8+RasEk1ACFLgMUqAaDuQ/Nk+mQA==} engines: {node: '>=v14.16.0'} @@ -14159,7 +14150,6 @@ packages: jschardet: 3.0.0 private-ip: 2.3.3 trace-redirect: 1.0.6 - dev: false github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658: resolution: {tarball: https://codeload.github.com/sampotts/plyr/tar.gz/d434c9af16e641400aaee93188594208d88f2658}