feat(frontend/MkUrlPreview): oEmbedのサポート (#10306)

* feat(frontend/MkUrlPreview): oEmbedのサポート

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* playerとoEmbedの統合

* Update CHANGELOG.md

* loading=lazyはここでは不要

* border: 0

* プレビュー直後にautoplayできる機能の復旧

* add test

* refactor test

* explain about cache

* expandPreviewはもう使わない

* summaly v4

* update summaly

* scrolling=no to fix pixiv

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
Kagami Sascha Rosylight 2023-03-19 08:59:31 +01:00 committed by GitHub
parent 4d73080da1
commit c091d9e6d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 207 additions and 60 deletions

View File

@ -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オブジェクトを入力してフェッチする機能とユーザーやートの検索機能を分離

View File

@ -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}`);

View File

@ -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"
}

View File

@ -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;

View File

@ -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';

View File

@ -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');
});
});

View File

@ -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}