aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-05-19 02:34:18 +0000
committerGitHub <noreply@github.com>2022-05-19 02:34:18 +0000
commit9b714fe39c83794538e0b38a63afd2bf39664c2f (patch)
treee0b638cad2b38c17f84fe5270f360a21756f6728
parent41dc70e3af49fc11383d0dff3bbe8f006272bed9 (diff)
parent97688e22c8335642b72d200ce0a865353bef5207 (diff)
downloadskyblock-stats-9b714fe39c83794538e0b38a63afd2bf39664c2f.tar.gz
skyblock-stats-9b714fe39c83794538e0b38a63afd2bf39664c2f.tar.bz2
skyblock-stats-9b714fe39c83794538e0b38a63afd2bf39664c2f.zip
Merge pull request #3 from skyblockstats/auctions
Auction Prices
-rw-r--r--package.json6
-rw-r--r--src/lib/APITypes.d.ts20
-rw-r--r--src/lib/AuctionPreviewTooltip.svelte56
-rw-r--r--src/lib/AuctionPriceScatterplot.svelte138
-rw-r--r--src/lib/GlobalTooltip.ts2
-rw-r--r--src/lib/api.ts1
-rw-r--r--src/lib/utils.ts10
-rw-r--r--src/routes/auctionprices.svelte210
-rw-r--r--src/routes/index.svelte2
-rw-r--r--svelte.config.js11
-rw-r--r--yarn.lock12
11 files changed, 452 insertions, 16 deletions
diff --git a/package.json b/package.json
index b2ba8a9..a2b2139 100644
--- a/package.json
+++ b/package.json
@@ -24,9 +24,7 @@
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
- "svelte": "^3.46.4",
"svelte-check": "^2.4.6",
- "svelte-preprocess": "^4.10.4",
"tslib": "^2.3.1",
"typescript": "~4.6.4",
"vite": "^2.8.6",
@@ -38,10 +36,12 @@
"@sveltejs/adapter-node": "^1.0.0-next.68",
"@sveltejs/adapter-static": "^1.0.0-next.28",
"@sveltejs/adapter-vercel": "^1.0.0-next.43",
- "@sveltejs/kit": "^1.0.0-next.330",
+ "@sveltejs/kit": "^1.0.0-next.335",
"cookie": "^0.5.0",
"dotenv": "^16.0.0",
"skyblock-assets": "^2.0.8",
+ "svelte": "^3.48.0",
+ "svelte-preprocess": "^4.10.6",
"typed-hypixel-api": "^1.1.0"
},
"packageManager": "yarn@3.1.1"
diff --git a/src/lib/APITypes.d.ts b/src/lib/APITypes.d.ts
index 6402721..e01a950 100644
--- a/src/lib/APITypes.d.ts
+++ b/src/lib/APITypes.d.ts
@@ -434,3 +434,23 @@ export interface AccessoryBagUpgrades {
list: string[]
}
}
+
+export interface SimpleAuctionSchema {
+ /** The UUID of the auction so we can look it up later. */
+ id: string
+ coins: number
+ /**
+ * The timestamp as **seconds** since epoch. It's in seconds instead of ms
+ * since we don't need to be super exact and so it's shorter.
+ */
+ ts: number
+ /** Whether the auction was bought or simply expired. */
+ success: boolean
+ bin: boolean
+}
+export interface ItemAuctionsSchema {
+ /** The id of the item */
+ id: string
+ sbId: string
+ auctions: SimpleAuctionSchema[]
+}
diff --git a/src/lib/AuctionPreviewTooltip.svelte b/src/lib/AuctionPreviewTooltip.svelte
new file mode 100644
index 0000000..26b01d9
--- /dev/null
+++ b/src/lib/AuctionPreviewTooltip.svelte
@@ -0,0 +1,56 @@
+<script lang="ts">
+ import type { PreviewedAuctionData } from './utils'
+ import { fade } from 'svelte/transition'
+
+ export let preview: PreviewedAuctionData | null
+ let lastPreview: PreviewedAuctionData | null
+
+ $: {
+ lastPreview = preview ?? lastPreview
+ }
+
+ function onMouseMove(e: MouseEvent) {
+ // commented out because it doesn't work: sometimes e.target is null when we click a point
+ if (e.target && !(e.target as HTMLElement).closest('.item-auction-history')) {
+ preview = null
+ lastPreview = null
+ }
+ }
+</script>
+
+<svelte:body on:mousemove={onMouseMove} />
+
+{#if lastPreview}
+ <div
+ id="auction-preview-tooltip-container"
+ style={lastPreview ? `left: ${lastPreview.pageX}px; top: ${lastPreview.pageY}px` : undefined}
+ out:fade={{ duration: 100 }}
+ in:fade={{ duration: 100 }}
+ >
+ <div id="auction-preview-tooltip">
+ <p><b>{lastPreview.auction.coins.toLocaleString()}</b> coins</p>
+ <time>{new Date(lastPreview.auction.ts * 1000).toLocaleString()}</time>
+ </div>
+ </div>
+{/if}
+
+<style>
+ #auction-preview-tooltip-container {
+ position: absolute;
+ pointer-events: none;
+ transition: left 200ms linear, top 200ms linear;
+ }
+ #auction-preview-tooltip {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(0, 0, 0, 0.1);
+ padding: 0.5em;
+ }
+
+ p {
+ margin: 0;
+ }
+
+ time {
+ color: var(--theme-darker-text);
+ }
+</style>
diff --git a/src/lib/AuctionPriceScatterplot.svelte b/src/lib/AuctionPriceScatterplot.svelte
new file mode 100644
index 0000000..6c59e70
--- /dev/null
+++ b/src/lib/AuctionPriceScatterplot.svelte
@@ -0,0 +1,138 @@
+<script lang="ts">
+ import { browser } from '$app/env'
+
+ import type { ItemAuctionsSchema, SimpleAuctionSchema } from './APITypes'
+ import type { PreviewedAuctionData } from './utils'
+
+ export let item: ItemAuctionsSchema
+ export let currentlyPreviewedAuction: PreviewedAuctionData | null
+
+ let svgEl: SVGElement
+ let maxCoins: number = item.auctions.reduce((max, auction) => Math.max(max, auction.coins), 0)
+ let currentTimestamp = Math.floor(Date.now() / 1000)
+ let earliestTimestamp = item.auctions.length > 0 ? item.auctions[0].ts : 0
+ let hoursBetween = (currentTimestamp - earliestTimestamp) / (60 * 60)
+ const gridWidth = 100 / hoursBetween
+
+ // this code is bad but it works
+ let heightCoinInterval = Math.ceil(Math.pow(10, Math.floor(Math.log10(maxCoins / 5))))
+ if (heightCoinInterval < maxCoins / 20) {
+ heightCoinInterval *= 5
+ } else if (heightCoinInterval < maxCoins / 10) {
+ heightCoinInterval *= 2
+ }
+ const gridHeight = 100 / (maxCoins / heightCoinInterval)
+
+ function getAuctionCoordinates(auction: SimpleAuctionSchema) {
+ const timestampPercentage =
+ (auction.ts - earliestTimestamp) / (currentTimestamp - earliestTimestamp)
+ return [timestampPercentage * 100, 100 - (auction.coins / maxCoins) * 100]
+ }
+
+ function updateNearest(e: MouseEvent) {
+ const rect = svgEl.getBoundingClientRect()
+
+ const mouseCoords = [e.clientX - rect.left, e.clientY - rect.top]
+ let nearestDistance = Number.MAX_SAFE_INTEGER
+ let nearestAuction: SimpleAuctionSchema | null = null
+ for (const auction of item.auctions) {
+ const auctionCoordsSvg = getAuctionCoordinates(auction)
+ const auctionCoords = [
+ (auctionCoordsSvg[0] * rect.width) / 100,
+ (auctionCoordsSvg[1] * rect.height) / 100,
+ ]
+ const distance =
+ Math.pow(mouseCoords[0] - auctionCoords[0], 2) +
+ Math.pow(mouseCoords[1] - auctionCoords[1], 2)
+ if (distance < nearestDistance) {
+ nearestDistance = distance
+ nearestAuction = auction
+ }
+ }
+ if (nearestAuction) {
+ const [svgX, svgY] = getAuctionCoordinates(nearestAuction)
+ const [x, y] = [(svgX * rect.width) / 100, (svgY * rect.height) / 100]
+ if (currentlyPreviewedAuction?.auction.id === nearestAuction.id) return
+ currentlyPreviewedAuction = {
+ pageX: window.scrollX + rect.left + x,
+ pageY: window.scrollY + rect.top + y,
+ auction: nearestAuction,
+ }
+ for (const el of document.getElementsByClassName('selected-auction'))
+ el.classList.remove('selected-auction')
+ document
+ .getElementsByClassName(`auction-point-${nearestAuction.id}`)[0]
+ .classList.add('selected-auction')
+ } else {
+ currentlyPreviewedAuction = null
+ }
+ }
+
+ function shortenBigNumber(n: number) {
+ if (n < 1000) return n
+ if (n < 1_000_000) return parseFloat((n / 1000).toPrecision(2)).toLocaleString() + 'k'
+ if (n < 1_000_000_000) return parseFloat((n / 1_000_000).toPrecision(2)).toLocaleString() + 'M'
+ if (n < 1_000_000_000_000)
+ return parseFloat((n / 1_000_000_000).toPrecision(2)).toLocaleString() + 'B'
+ }
+</script>
+
+<svg viewBox="0 0 100 100" class="item-auction-history">
+ <defs>
+ <pattern
+ id="grid-{item.id}"
+ width={gridWidth}
+ height={gridHeight}
+ patternUnits="userSpaceOnUse"
+ x="0%"
+ y="100%"
+ >
+ <path
+ d="M {gridWidth} {gridHeight} L 0 {gridHeight} 0 0"
+ fill="none"
+ stroke="#fff2"
+ stroke-width="1"
+ />
+ </pattern>
+ </defs>
+ {#each new Array(Math.floor(maxCoins / heightCoinInterval) + 1) as _, intervalIndex}
+ <text
+ x="-1"
+ y={Math.min(Math.max(5, 100 - intervalIndex * gridHeight + 2), 100)}
+ fill="var(--theme-darker-text)"
+ font-size="6px"
+ text-anchor="end">{shortenBigNumber(heightCoinInterval * intervalIndex)}</text
+ >
+ {/each}
+ <rect
+ width="100%"
+ height="100%"
+ fill="url(#grid-{item.id})"
+ on:mousemove={updateNearest}
+ bind:this={svgEl}
+ />
+
+ {#each item.auctions as auction (auction.id)}
+ {@const [x, y] = getAuctionCoordinates(auction)}
+ <circle
+ cx={x}
+ cy={y}
+ r="1"
+ stroke-width="4"
+ fill={auction.bin ? '#11b' : '#1b1'}
+ class="auction-point-{auction.id}"
+ />
+ <!-- class:selected-auction={currentlyPreviewedAuction?.auction?.id === auction?.id} -->
+ {/each}
+</svg>
+
+<style>
+ .item-auction-history {
+ height: 10em;
+ width: 100%;
+ }
+
+ svg :global(.selected-auction) {
+ stroke: #06e7;
+ }
+</style>
diff --git a/src/lib/GlobalTooltip.ts b/src/lib/GlobalTooltip.ts
index d2c1020..f4bc381 100644
--- a/src/lib/GlobalTooltip.ts
+++ b/src/lib/GlobalTooltip.ts
@@ -52,7 +52,6 @@ export function registerItem(itemEl: HTMLElement) {
})
itemEl.addEventListener('click', e => {
tooltipLocked = !tooltipLocked
- moveTooltipToMouse(e)
tooltipEl.style.display = 'block'
if (tooltipLocked) {
tooltipEl.style.userSelect = 'auto'
@@ -64,6 +63,7 @@ export function registerItem(itemEl: HTMLElement) {
const loreHtml = itemEl.getElementsByClassName('tooltip-lore')[0].innerHTML
const nameHtml = itemEl.getElementsByClassName('tooltip-name')[0].innerHTML
tooltipEl.innerHTML = `<p class="item-lore-name">${nameHtml}</p><p class="item-lore-text">${loreHtml}</p>`
+ moveTooltipToMouse(e)
})
document.addEventListener('mousedown', e => {
if (tooltipLocked && !tooltipEl.contains(e.target as Node)) {
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 689a952..e3559e1 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1,2 +1,3 @@
// the trailing slash is required
export const API_URL = 'https://skyblock-api.matdoes.dev/'
+// export const API_URL = 'http://localhost:8080/' \ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index e34d573..c094db4 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,3 +1,5 @@
+import type { SimpleAuctionSchema } from "./APITypes"
+
export const colorCodes: { [key: string]: string } = {
'0': '#000000', // black
'1': '#0000be', // blue
@@ -219,4 +221,10 @@ export function skyblockTime(year: number, month = 1, day = 1) {
if (month) time += 37200000 * (month - 1)
if (day) time += 1200000 * (day - 1)
return time
-} \ No newline at end of file
+}
+
+export interface PreviewedAuctionData {
+ pageX: number
+ pageY: number
+ auction: SimpleAuctionSchema
+}
diff --git a/src/routes/auctionprices.svelte b/src/routes/auctionprices.svelte
new file mode 100644
index 0000000..9e7615c
--- /dev/null
+++ b/src/routes/auctionprices.svelte
@@ -0,0 +1,210 @@
+<script lang="ts" context="module">
+ import type { Load } from '@sveltejs/kit'
+ import { API_URL } from '$lib/api'
+
+ export const load: Load = async ({ params, fetch }) => {
+ const auctionItemsPromise = fetch(`${API_URL}auctionitems`).then(r => r.json())
+ const data = await fetch(`${API_URL}auctionprices`).then(r => r.json())
+ const auctionItems = await auctionItemsPromise
+
+ return {
+ props: {
+ data,
+ auctionItems,
+ },
+ }
+ }
+</script>
+
+<script lang="ts">
+ import Header from '$lib/Header.svelte'
+ import Head from '$lib/Head.svelte'
+ import { cleanId, removeFormattingCode, toTitleCase, type PreviewedAuctionData } from '$lib/utils'
+ import type { ItemAuctionsSchema } from '$lib/APITypes'
+ import AuctionPriceScatterplot from '$lib/AuctionPriceScatterplot.svelte'
+ import AuctionPreviewTooltip from '$lib/AuctionPreviewTooltip.svelte'
+ import { browser } from '$app/env'
+ import Item from '$lib/minecraft/Item.svelte'
+ import furfskyReborn from 'skyblock-assets/matchers/furfsky_reborn.json'
+
+ export let data: ItemAuctionsSchema[]
+ export let auctionItems: Record<string, { display: { name: string }; vanillaId?: string }>
+
+ let currentlyPreviewedAuction: PreviewedAuctionData | null = null
+
+ let query: string = ''
+
+ $: queryNormalized = query.toLowerCase()
+
+ let allMatchingItemIds: string[]
+ $: {
+ pageNumber = 0
+ allMatchingItemIds = Object.entries(auctionItems)
+ .filter(a => removeFormattingCode(a[1]?.display.name.toLowerCase()).includes(queryNormalized))
+ .map(a => a[0])
+ }
+ $: {
+ if (browser) fetchAndSetItems(allMatchingItemIds.slice(0, 100))
+ }
+
+ async function fetchAndSetItems(itemIds: string[]) {
+ const localQuery = query
+ const localData = await fetchItems(query.length > 0 ? itemIds : null)
+ // if the query hasn't changed, update the data
+ if (query === localQuery) data = localData
+ }
+ async function fetchItems(itemIds: null | string[]): Promise<ItemAuctionsSchema[]> {
+ let url = `${API_URL}auctionprices`
+ if (itemIds !== null) {
+ if (itemIds.length === 0) return []
+ url += `?items=${itemIds.join(',')}`
+ }
+ return await fetch(url).then(r => r.json())
+ }
+
+ let pageHeight = 0
+ $: {
+ pageHeight = 0
+ }
+
+ // 0 indexed
+ let pageNumber = 0
+ let loadingPage = false
+
+ async function checkScroll() {
+ if (loadingPage) return
+
+ let pageHeightTemp = window.scrollY + window.innerHeight
+ if (pageHeightTemp <= pageHeight) return
+ pageHeight = pageHeightTemp
+ if (pageHeight >= document.body.scrollHeight - 1000) {
+ loadingPage = true
+ pageNumber++
+ const itemIds = allMatchingItemIds.slice(pageNumber * 100, (pageNumber + 1) * 100)
+ if (itemIds.length > 0) {
+ const shownIds = data.map(d => d.id)
+ const items = (await fetchItems(itemIds)).filter(i => !shownIds.includes(i.id))
+ if (items.length > 0) data = [...data, ...items]
+ }
+ loadingPage = false
+ }
+ }
+
+ $: {
+ if (browser && !currentlyPreviewedAuction) {
+ for (const el of document.getElementsByClassName('selected-auction'))
+ el.classList.remove('selected-auction')
+ }
+ }
+</script>
+
+<Head title="SkyBlock Auction Prices" />
+<Header />
+
+<svelte:window on:scroll={checkScroll} />
+
+<AuctionPreviewTooltip bind:preview={currentlyPreviewedAuction} />
+
+<main>
+ <h1>SkyBlock Auction Prices</h1>
+ <div class="filter-items-settings">
+ <input type="text" id="filter-items-tier" placeholder="Search..." bind:value={query} />
+ </div>
+ <div class="item-list">
+ {#each data as item (item.id)}
+ {@const binAuctions = item.auctions.filter(i => i.bin)}
+ {@const normalAuctions = item.auctions.filter(i => !i.bin)}
+ {@const itemData = auctionItems[item.sbId]}
+ <div class="item-container">
+ {#if itemData && itemData.vanillaId}
+ <div class="item-slot-container">
+ <Item item={{ ...itemData, id: item.id }} pack={furfskyReborn} headSize={50} isslot />
+ </div>
+ {/if}
+ <h2>
+ {removeFormattingCode(
+ auctionItems[item.id]?.display.name ?? toTitleCase(cleanId(item.id.toLowerCase()))
+ )}
+ </h2>
+ <div class="auctions-info-text">
+ {#if binAuctions.length > 0}
+ <p>
+ Cheapest recent BIN: <b>
+ {binAuctions.reduce((a, b) => (a.coins < b.coins ? a : b)).coins.toLocaleString()} coins
+ </b>
+ </p>
+ {/if}
+ {#if normalAuctions.length > 0}
+ <p>
+ Cheapest recent auction: <b>
+ {normalAuctions
+ .reduce((a, b) => (a.coins < b.coins ? a : b))
+ .coins.toLocaleString()} coins
+ </b>
+ </p>
+ {/if}
+ {#if item.auctions.length >= 2}
+ <p>
+ Median:
+ <b>
+ {[...item.auctions]
+ .sort((a, b) => a.coins - b.coins)
+ [Math.floor(item.auctions.length / 2)].coins.toLocaleString()} coins
+ </b>
+ </p>
+ <p>
+ Volume:
+ <b>
+ {parseFloat(
+ (
+ (24 * 60 * 60) /
+ ((Date.now() / 1000 - item.auctions[0].ts) / item.auctions.length)
+ ).toPrecision(2)
+ ).toLocaleString()}/day
+ </b>
+ </p>
+ {/if}
+ </div>
+ <div class="item-scatterplot">
+ <AuctionPriceScatterplot {item} bind:currentlyPreviewedAuction />
+ </div>
+ </div>
+ {/each}
+ {#if data.length === 0}
+ No results
+ {/if}
+ </div>
+</main>
+
+<style>
+ p {
+ margin: 0;
+ }
+ .item-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
+ grid-gap: 1em;
+ margin-top: 1em;
+ }
+ .item-container {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(0, 0, 0, 0.1);
+ padding: 0.75em;
+ border-radius: 1em;
+ }
+
+ .item-scatterplot {
+ margin-top: 1em;
+ }
+
+ .auctions-info-text {
+ color: var(--theme-darker-text);
+ }
+ .auctions-info-text b {
+ color: var(--theme-main-text);
+ }
+
+ .item-slot-container {
+ float: right;
+ }
+</style>
diff --git a/src/routes/index.svelte b/src/routes/index.svelte
index 4c11a88..d22f854 100644
--- a/src/routes/index.svelte
+++ b/src/routes/index.svelte
@@ -49,7 +49,7 @@
<section>
<h2>Other SkyBlock tools</h2>
<ul id="other-tools-list">
- <li><a>Auction prices (coming soon)</a></li>
+ <li><a href="/auctionprices">Auction prices</a></li>
<li><a href="/leaderboards">Leaderboards</a></li>
<li><a>Bazaar (coming soon)</a></li>
<li><a href="/chat">Fake chat generator</a></li>
diff --git a/svelte.config.js b/svelte.config.js
index b4cc10d..8d97638 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -45,10 +45,13 @@ const config = {
server:
process.env.REPL_ID || process.env.GITPOD_WORKSPACE_ID
? {
- hmr: {
- protocol: 'wss',
- port: 443,
- },
+ hmr: process.env.GITPOD_WORKSPACE_URL
+ ? {
+ host: process.env.GITPOD_WORKSPACE_URL.replace('https://', '3000-'),
+ protocol: "wss",
+ clientPort: 443
+ }
+ : true
}
: {},
},
diff --git a/yarn.lock b/yarn.lock
index 758e549..dad6890 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -150,10 +150,10 @@
dependencies:
esbuild "^0.14.21"
-"@sveltejs/kit@^1.0.0-next.330":
- version "1.0.0-next.330"
- resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.0.0-next.330.tgz#1865ca9e38eb34633b0e469e833f4f51ebb5eb69"
- integrity sha512-Wb95D5tOF8BViZuikqzZLAcupdS7TpXtadNPgpEOxKowmkrW8xjRrrfVdPIkNOLqAP1V+gKInmQ/gFYmnv5EjA==
+"@sveltejs/kit@^1.0.0-next.335":
+ version "1.0.0-next.335"
+ resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.0.0-next.335.tgz#14bd4016633605b9edc8b7a77cd393ca778449db"
+ integrity sha512-iZutvIJSSNJJGceZOX2ZWqcyRqp9MIPnNWOgOLXqBG/Z/+KLoN8MRI0U79XIw232SAEXhhkwaJtB3UnXQSu85A==
dependencies:
"@sveltejs/vite-plugin-svelte" "^1.0.0-next.32"
chokidar "^3.5.3"
@@ -1608,7 +1608,7 @@ svelte-hmr@^0.14.11:
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.11.tgz#63d532dc9c2c849ab708592f034765fa2502e568"
integrity sha512-R9CVfX6DXxW1Kn45Jtmx+yUe+sPhrbYSUp7TkzbW0jI5fVPn6lsNG9NEs5dFg5qRhFNAoVdRw5qQDLALNKhwbQ==
-svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.4:
+svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.6:
version "4.10.6"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.6.tgz#5f9a53e7ed3b85fc7e0841120c725b76ac5a1ba8"
integrity sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ==
@@ -1620,7 +1620,7 @@ svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.4:
sorcery "^0.10.0"
strip-indent "^3.0.0"
-svelte@^3.46.4:
+svelte@^3.48.0:
version "3.48.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.48.0.tgz#f98c866d45e155bad8e1e88f15f9c03cd28753d3"
integrity sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==