├── demo ├── favicon-32x32.png ├── apple-touch-icon.png ├── index.js ├── index.html └── style.css ├── package ├── icons │ ├── ambank.png │ ├── google.png │ ├── kraken.png │ ├── reddit.png │ ├── vscode.png │ ├── deepseek.png │ ├── instagram.png │ ├── mangafire.png │ ├── pixabay.png │ ├── postman.png │ ├── shopify.png │ ├── twitter.png │ ├── youtube.png │ ├── docs.google.png │ ├── icloud.mail.png │ ├── keep.google.ico │ ├── mail.google.png │ ├── maps.google.png │ ├── coinmarketcap.png │ ├── drive.google.png │ ├── music.google.png │ ├── photos.google.png │ ├── calendar.google.png │ ├── messages.google.png │ ├── podcasts.google.png │ ├── teams.microsoft.png │ ├── designer.microsoft.png │ ├── leboncoin.svg │ ├── discord.svg │ ├── grok.svg │ ├── spotify.svg │ ├── gemini.google.svg │ ├── localhost.svg │ ├── whatsapp.svg │ ├── claude.svg │ ├── notfound.svg │ ├── amazon.svg │ ├── sheets.google.svg │ ├── copilot.microsoft.svg │ ├── openai.svg │ └── outlook.svg ├── tests │ ├── icons.test.ts │ ├── examples.test.ts │ └── fetch.test.ts └── src │ ├── helpers.ts │ ├── fetchers.ts │ ├── icons.ts │ ├── parsers.ts │ └── index.ts ├── .gitignore ├── .github └── workflows │ └── test.yaml ├── deno.json ├── package.json ├── LICENSE ├── README.md └── deno.lock /demo/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/demo/favicon-32x32.png -------------------------------------------------------------------------------- /package/icons/ambank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/ambank.png -------------------------------------------------------------------------------- /package/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/google.png -------------------------------------------------------------------------------- /package/icons/kraken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/kraken.png -------------------------------------------------------------------------------- /package/icons/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/reddit.png -------------------------------------------------------------------------------- /package/icons/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/vscode.png -------------------------------------------------------------------------------- /demo/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/demo/apple-touch-icon.png -------------------------------------------------------------------------------- /package/icons/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/deepseek.png -------------------------------------------------------------------------------- /package/icons/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/instagram.png -------------------------------------------------------------------------------- /package/icons/mangafire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/mangafire.png -------------------------------------------------------------------------------- /package/icons/pixabay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/pixabay.png -------------------------------------------------------------------------------- /package/icons/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/postman.png -------------------------------------------------------------------------------- /package/icons/shopify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/shopify.png -------------------------------------------------------------------------------- /package/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/twitter.png -------------------------------------------------------------------------------- /package/icons/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/youtube.png -------------------------------------------------------------------------------- /package/icons/docs.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/docs.google.png -------------------------------------------------------------------------------- /package/icons/icloud.mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/icloud.mail.png -------------------------------------------------------------------------------- /package/icons/keep.google.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/keep.google.ico -------------------------------------------------------------------------------- /package/icons/mail.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/mail.google.png -------------------------------------------------------------------------------- /package/icons/maps.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/maps.google.png -------------------------------------------------------------------------------- /package/icons/coinmarketcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/coinmarketcap.png -------------------------------------------------------------------------------- /package/icons/drive.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/drive.google.png -------------------------------------------------------------------------------- /package/icons/music.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/music.google.png -------------------------------------------------------------------------------- /package/icons/photos.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/photos.google.png -------------------------------------------------------------------------------- /package/icons/calendar.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/calendar.google.png -------------------------------------------------------------------------------- /package/icons/messages.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/messages.google.png -------------------------------------------------------------------------------- /package/icons/podcasts.google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/podcasts.google.png -------------------------------------------------------------------------------- /package/icons/teams.microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/teams.microsoft.png -------------------------------------------------------------------------------- /package/icons/designer.microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victrme/favicon-fetcher/HEAD/package/icons/designer.microsoft.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .netlify 3 | .wrangler 4 | .DS_Store 5 | deno.lock 6 | node_modules 7 | dist 8 | example 9 | coverage 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: denoland/setup-deno@v2 11 | with: 12 | deno-version: v2.x 13 | 14 | - run: | 15 | deno install 16 | deno test --allow-net 17 | -------------------------------------------------------------------------------- /package/tests/icons.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect" 2 | 3 | import STATIC_ICONS from "../src/icons.ts" 4 | 5 | Deno.test("Static icons has valid type", function () { 6 | expect(typeof STATIC_ICONS.LIST).toBe("object") 7 | 8 | for (const [path, match] of Object.entries(STATIC_ICONS.LIST)) { 9 | expect(typeof path).toBe("string") 10 | expect(typeof match).toBe("object") 11 | expect(match.length).toBeGreaterThan(0) 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json", 3 | "name": "@victr/favicon-fetcher", 4 | "version": "4.0.0", 5 | "license": "MIT", 6 | "exports": "./package/src/index.ts", 7 | "nodeModulesDir": "auto", 8 | "imports": { 9 | "@std/expect": "jsr:@std/expect@^1.0.16" 10 | }, 11 | "fmt": { 12 | "singleQuote": false, 13 | "semiColons": false, 14 | "indentWidth": 4, 15 | "lineWidth": 144, 16 | "useTabs": true 17 | }, 18 | "lint": { 19 | "rules": { 20 | "tags": ["recommended"], 21 | "exclude": ["no-sloppy-imports"] 22 | } 23 | }, 24 | "unstable": ["sloppy-imports"] 25 | } 26 | -------------------------------------------------------------------------------- /package/icons/leboncoin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package/icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /package/icons/grok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@victr/favicon-fetcher", 3 | "version": "4.0.0", 4 | "author": "Victor Azevedo", 5 | "description": "Favicon fetcher finds favicons and flawlessly fetches them fast", 6 | "license": "MIT", 7 | "keywords": [ 8 | "favicon" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/victrme/favicon-fetcher" 13 | }, 14 | "scripts": { 15 | "demo": "npx http-server ./demo -c-1", 16 | "dev": "npx wrangler dev ./package/src/index.ts", 17 | "build": "tsup" 18 | }, 19 | "devDependencies": { 20 | "tsup": "^8.5.0", 21 | "typescript": "^5.8.3" 22 | }, 23 | "files": [ 24 | "/dist" 25 | ], 26 | "exports": { 27 | ".": { 28 | "import": { 29 | "types": "./dist/index.d.ts", 30 | "default": "./dist/index.js" 31 | } 32 | } 33 | }, 34 | "tsup": { 35 | "entry": [ 36 | "package/src/index.ts" 37 | ], 38 | "format": "esm", 39 | "dts": true, 40 | "clean": true 41 | }, 42 | "type": "module" 43 | } 44 | -------------------------------------------------------------------------------- /package/icons/spotify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /package/icons/gemini.google.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /package/icons/localhost.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | localhost 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | globalThis.addEventListener("DOMContentLoaded", () => { 4 | document.getElementById("search-form")?.addEventListener("submit", submitSearch) 5 | document.getElementById("favicon-img")?.addEventListener("click", openBigIconModal) 6 | document.getElementById("big-icon-modal")?.addEventListener("click", closeBigIconModal) 7 | }) 8 | 9 | async function submitSearch(event) { 10 | event.preventDefault() 11 | 12 | const big = document.getElementById("big-icon-modal_favicon-img") 13 | const img = document.getElementById("favicon-img") 14 | const input = document.getElementById("search-input") 15 | const mockimg = document.createElement("img") 16 | 17 | if (input.value.length < 4) { 18 | return 19 | } 20 | 21 | img.classList.add("loading") 22 | 23 | const value = input.value.startsWith("http") ? input.value : `https://${input.value}` 24 | const resp = await fetch("https://api.favicon.victr.me/text/" + value) 25 | const icon = await resp.text() 26 | 27 | mockimg.src = icon 28 | await new Promise((r) => mockimg.addEventListener("load", r)) 29 | 30 | img.src = icon 31 | big.src = icon 32 | img.classList.remove("loading") 33 | 34 | input.blur() 35 | } 36 | 37 | function openBigIconModal() { 38 | document.getElementById("big-icon-modal")?.showModal() 39 | } 40 | 41 | function closeBigIconModal(event) { 42 | if (event.target?.tagName === "DIALOG") { 43 | document.getElementById("big-icon-modal")?.close() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import STATIC_ICONS from "./icons" 2 | import type { Head, Icon } from "./parsers" 3 | 4 | export function sortClosestToSize(icons: Icon[], val: number): Icon[] { 5 | const sorted = icons.sort((a, b) => Math.abs(a.size - val) - Math.abs(b.size - val)) 6 | return sorted 7 | } 8 | 9 | export function sizesToNumber(str = ""): number { 10 | return parseInt(str?.split("x")[0]) || 48 11 | } 12 | 13 | export function getIconFromList(query: string): string | undefined { 14 | const iconList = Object.entries(STATIC_ICONS.LIST) 15 | 16 | for (const [path, matches] of iconList) { 17 | for (const match of matches) { 18 | if (query.includes(match)) return path 19 | } 20 | } 21 | } 22 | 23 | // Loggers & debuggers 24 | 25 | let canLog = false 26 | let canDebug = false 27 | let debugList: Debug = {} 28 | 29 | export function initLog(val: boolean): void { 30 | canLog = val 31 | } 32 | 33 | export function initDebug(): void { 34 | debugList = {} 35 | canDebug = true 36 | } 37 | 38 | export function toDebug(key: keyof Debug, value: any) { 39 | if (canDebug) { 40 | debugList[key] = value 41 | } 42 | } 43 | 44 | export function toLog(...logs: string[]) { 45 | if (canLog) { 46 | logs.forEach(console.error) 47 | } 48 | } 49 | 50 | export function getDebug(): Debug { 51 | return debugList 52 | } 53 | 54 | export interface Debug { 55 | html?: string 56 | head?: Head 57 | metas?: string[] 58 | links?: string[] 59 | manifest?: string[] 60 | paths?: string[] 61 | } 62 | -------------------------------------------------------------------------------- /package/tests/examples.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect" 2 | 3 | import STATIC_ICONS from "../src/icons.ts" 4 | import favicon from "../src/index.ts" 5 | 6 | const NOTFOUND = `${STATIC_ICONS.HOST}notfound.svg` 7 | 8 | Deno.test("ikea.com", async function () { 9 | const request = new Request("http://localhost/text/https://ikea.com") 10 | const response = await favicon.fetch(request) 11 | 12 | expect(response.status).toBe(200) 13 | expect((await response.text()) !== NOTFOUND).toBe(true) 14 | }) 15 | 16 | Deno.test("vitest.dev", async function () { 17 | const request = new Request("http://localhost/text/https://vitest.dev") 18 | const response = await favicon.fetch(request) 19 | 20 | expect(response.status).toBe(200) 21 | expect((await response.text()) !== NOTFOUND).toBe(true) 22 | }) 23 | 24 | Deno.test("steamcharts.com", async function () { 25 | const request = new Request( 26 | "http://localhost/text/https://steamcharts.com", 27 | ) 28 | const response = await favicon.fetch(request) 29 | 30 | expect(response.status).toBe(200) 31 | expect((await response.text()) !== NOTFOUND).toBe(true) 32 | }) 33 | 34 | Deno.test("guide.michelin.com", async function () { 35 | const request = new Request("http://localhost/text/https://guide.michelin.com/fr/fr/restaurants") 36 | const response = await favicon.fetch(request) 37 | 38 | expect(response.status).toBe(200) 39 | expect((await response.text()) !== NOTFOUND).toBe(true) 40 | }) 41 | 42 | Deno.test("microsoftedge.microsoft.com", async function () { 43 | const url = "http://localhost/text/https://microsoftedge.microsoft.com/addons/detail/ublock-origin/odfafepnkmbhccpbejgmiehpchacaeak" 44 | const request = new Request(url) 45 | const response = await favicon.fetch(request) 46 | 47 | expect(response.status).toBe(200) 48 | expect((await response.text()) !== NOTFOUND).toBe(true) 49 | }) 50 | -------------------------------------------------------------------------------- /package/icons/whatsapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Favicon Fetcher 2 | 3 | Favicon fetcher finds sites favicons and fetches them fast 4 | 5 | - Test it here: https://favicon.victr.me 6 | - On JSR: https://jsr.io/@victr/favicon-fetcher 7 | - On NPM: https://www.npmjs.com/package/@victr/favicon-fetcher 8 | 9 | ## Use 10 | 11 | ```ts 12 | import favicon from "@victr/favicon-fetcher" 13 | 14 | window.onload = async function () { 15 | const img = document.getElementById("some-id") 16 | const url = await favicon.text("https://github.com") 17 | img.src = url 18 | } 19 | ``` 20 | 21 | ### Types 22 | 23 | ```ts 24 | interface Default { 25 | text: (query: string, options?: Options) => Promise 26 | blob: (query: string, options?: Options) => Promise 27 | fetch: (request: Request) => Promise 28 | list: (query: string) => Promise 29 | debug: (query: string) => Promise 30 | } 31 | 32 | interface Options { 33 | log?: true 34 | check?: "all" | "best" | "none" 35 | } 36 | ``` 37 | 38 | ## Publish 39 | 40 | ```bash 41 | # Build first, using Deno & tsup 42 | deno i 43 | deno task build 44 | 45 | # ESM dist/index.js 8.95 KB 46 | # ESM ⚡️ Build success in 272ms 47 | # DTS ⚡️ Build success in 327ms 48 | # DTS dist/index.d.ts 1.20 KB 49 | ``` 50 | 51 | On npmjs.com 52 | 53 | ```bash 54 | npm publish --access public 55 | 56 | # npm notice 📦 @victr/favicon-fetcher@x.x.x 57 | # + @victr/favicon-fetcher@x.x.x 58 | ``` 59 | 60 | On jsr.io 61 | 62 | ```bash 63 | deno publish 64 | 65 | # Publishing @victr/favicon-fetcher@x.x.x ... 66 | # Successfully published @victr/favicon-fetcher@x.x.x 67 | ``` 68 | 69 | ## Cloudflare workers 70 | 71 | Use can easily deploy favicon-fetcher as a worker because it uses the same `export fetch()`.\ 72 | To do so: 73 | 74 | ```bash 75 | npm install --global wrangler 76 | 77 | # added 173 packages in 11s 78 | 79 | wrangler deploy ./package/src/index.ts --name favicon-fetcher --compatibility-date 2025-01-13 80 | 81 | # Total Upload: 9.70 KiB / gzip: 3.15 KiB 82 | # Uploaded favicon-fetcher (8.11 sec) 83 | ``` 84 | -------------------------------------------------------------------------------- /package/src/fetchers.ts: -------------------------------------------------------------------------------- 1 | import { toDebug, toLog } from "./helpers" 2 | 3 | interface fetchHtmlResponse { 4 | html?: string 5 | redirected?: string 6 | captchaProtected?: true 7 | } 8 | 9 | export interface Manifest { 10 | icons?: { 11 | src: string 12 | sizes: string 13 | }[] 14 | } 15 | 16 | const turnstileTitle = "Just a moment..." 17 | 18 | const headers: HeadersInit = { 19 | "Cache-Control": "max-age=0", 20 | "Accept-Language": "en-US;q=0.9,en;q=0.7", 21 | "Sec-Ch-Ua": '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', 22 | "Sec-Ch-Ua-Mobile": "?0", 23 | "Sec-Ch-Ua-Platform": '"macOS"', 24 | "Sec-Fetch-Dest": "document", 25 | "Sec-Fetch-Site": "none", 26 | "Sec-Fetch-User": "?1", 27 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", 28 | } 29 | 30 | export async function fetchHtml(url: string): Promise { 31 | const result: fetchHtmlResponse = {} 32 | 33 | try { 34 | const signal = AbortSignal.timeout(6000) 35 | const resp = await fetch(url, { headers, signal }) 36 | const html = await resp.text() 37 | 38 | result.html = html 39 | 40 | const isCaptcha = html.includes(turnstileTitle) 41 | const isRedirect = resp.redirected 42 | 43 | if (isCaptcha) { 44 | toDebug("html", html) 45 | return { captchaProtected: true } 46 | } 47 | 48 | if (isRedirect) { 49 | result.redirected = resp.url 50 | } 51 | } catch (_) { 52 | toLog(url, "Can't fetch HTML") 53 | } 54 | 55 | return result 56 | } 57 | 58 | export async function fetchManifest(url: string): Promise { 59 | try { 60 | const signal = AbortSignal.timeout(2000) 61 | const resp = await fetch(url, { headers, signal }) 62 | const json = await resp.json() 63 | return json 64 | } catch (_) { 65 | toLog(url, "Can't fetch manifest") 66 | } 67 | } 68 | 69 | export async function fetchIcon(url: string): Promise { 70 | try { 71 | const signal = AbortSignal.timeout(2000) 72 | const resp = await fetch(url, { signal, headers }) 73 | 74 | if (resp.status === 200) { 75 | const blob = await resp.blob() 76 | return blob 77 | } 78 | } catch (_) { 79 | toLog(url, "Can't fetch favicon") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package/tests/fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect" 2 | 3 | import STATIC_ICONS from "../src/icons.ts" 4 | import favicon from "../src/index.ts" 5 | 6 | const LOCALHOST = `${STATIC_ICONS.HOST}localhost.svg` 7 | const NOTFOUND = `${STATIC_ICONS.HOST}notfound.svg` 8 | 9 | Deno.test("returns notfound icon on bad query", async function () { 10 | const request = new Request( 11 | "http://0.0.0.0:0000/text/drgrdrdhwrdehrwjherwjh", 12 | ) 13 | const response = await favicon.fetch(request) 14 | expect(response.status).toBe(200) 15 | expect(await response.text()).toBe(NOTFOUND) 16 | }) 17 | 18 | Deno.test("returns notfound icon when no protocols are specified", async function () { 19 | const response = await favicon.fetch( 20 | new Request("http://0.0.0.0:0000/text/victr.me/"), 21 | ) 22 | expect(response.status).toBe(200) 23 | expect(await response.text()).toBe(NOTFOUND) 24 | }) 25 | 26 | Deno.test("returns 400 when no type are specified", async function () { 27 | const response = await favicon.fetch( 28 | new Request("http://0.0.0.0:0000/https://victr.me"), 29 | ) 30 | expect(response.status).toBe(404) 31 | }) 32 | 33 | Deno.test("gets favicon as text", async function () { 34 | const response = await favicon.fetch( 35 | new Request("http://0.0.0.0:0000/text/https://victr.me"), 36 | ) 37 | expect(response.status).toBe(200) 38 | expect(await response.text()).toBe( 39 | "https://victr.me/apple-touch-icon.png", 40 | ) 41 | }) 42 | 43 | Deno.test("gets favicon as blob", async function () { 44 | const response = await favicon.fetch( 45 | new Request("http://0.0.0.0:0000/blob/https://victr.me"), 46 | ) 47 | expect(response.status).toBe(200) 48 | expect((await response.blob())?.type).toBe("image/png") 49 | }) 50 | 51 | Deno.test("returns localhost icon with http://127.0.0.1", async function () { 52 | const response = await favicon.fetch( 53 | new Request("http://0.0.0.0:0000/text/http://127.0.0.1:8787"), 54 | ) 55 | expect(response.status).toBe(200) 56 | expect(await response.text()).toBe(LOCALHOST) 57 | }) 58 | 59 | Deno.test("returns localhost icon with http://localhost/", async function () { 60 | const response = await favicon.fetch( 61 | new Request("http://0.0.0.0:0000/text/http://localhost/"), 62 | ) 63 | 64 | expect(response.status).toBe(200) 65 | expect(await response.text()).toBe(LOCALHOST) 66 | }) 67 | -------------------------------------------------------------------------------- /package/icons/claude.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/src/icons.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | /** Static icons are retrieved from this URL */ 3 | HOST: "https://raw.githubusercontent.com/victrme/favicon-fetcher/main/package/icons/", 4 | 5 | /** List format is "filename": "urls to match" */ 6 | LIST: { 7 | /* Do not modify */ 8 | "localhost.svg": ["http://localhost", "localhost:", "http://127.0.0.1:", "127.0.0.1:"], 9 | 10 | /* Can be modified */ 11 | "vscode.png": ["vscode.dev"], 12 | "designer.microsoft.png": ["designer.microsoft.com"], 13 | "teams.microsoft.png": ["teams.microsoft.com"], 14 | "copilot.microsoft.svg": ["https://www.bing.com/images/create", "copilot.microsoft.com"], 15 | "twitter.png": ["twitter.com"], 16 | "music.google.png": ["music.youtube.com"], 17 | "youtube.png": ["youtube.com"], 18 | "instagram.png": ["instagram.com"], 19 | "whatsapp.svg": ["whatsapp.com"], 20 | "grok.svg": ["grok.com"], 21 | "claude.svg": ["claude.ai"], 22 | "openai.svg": ["chatgpt.com", "openai.com"], 23 | "discord.svg": ["discord.com"], 24 | "leboncoin.svg": ["leboncoin.fr"], 25 | "reddit.png": ["old.reddit.com", "reddit.com"], 26 | "spotify.svg": ["open.spotify.com", "spotify.com"], 27 | "google.png": ["://google.com", "www.google.com"], 28 | "mail.google.png": ["mail.google.com", "gmail.com"], 29 | "keep.google.ico": ["keep.google.com"], 30 | "docs.google.png": ["docs.google.com"], 31 | "maps.google.png": ["maps.google.com", "google.com/maps"], 32 | "contacts.google.png": ["contacts.google.com"], 33 | "messages.google.png": ["messages.google.com"], 34 | "podcasts.google.png": ["podcasts.google.com"], 35 | "calendar.google.png": ["calendar.google.com"], 36 | "gemini.google.svg": ["gemini.google.com"], 37 | "drive.google.png": ["drive.google.com"], 38 | "icloud.mail.png": ["icloud.com/mail/"], 39 | "sheets.google.svg": ["docs.google.com/spreadsheets"], 40 | "amazon.svg": [ 41 | "amazon.com", 42 | "amazon.ca", 43 | "amazon.fr", 44 | "amazon.de", 45 | "amazon.it", 46 | "amazon.co.uk", 47 | ], 48 | "ambank.png": ["amonline.com"], 49 | "postman.png": ["postman.co"], 50 | 51 | /* Captcha protected websites */ 52 | "shopify.png": ["shopify.com"], 53 | "mangafire.png": ["mangafire.to"], 54 | "deepseek.png": ["deepseek.com"], 55 | "coinmarketcap.png": ["coinmarketcap.com"], 56 | "pixabay.png": ["pixabay.com"], 57 | "kraken.png": ["kraken.com"], 58 | } as Record, 59 | } 60 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Favicon Fetcher: Find favicons and fetch them fast 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 38 | Favicon fetcher 39 |
40 | 41 | 44 |
45 |
46 | 47 |
48 |
49 | 52 | 55 | 58 |
59 |
60 | 69 |
70 |
71 |
72 | 73 |
74 |

https://www.wikipedia.org/static/apple-touch/wikipedia.png

75 |

32×32

76 |
77 | 78 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /package/icons/notfound.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /package/src/parsers.ts: -------------------------------------------------------------------------------- 1 | import { sizesToNumber, toDebug, toLog } from "./helpers" 2 | import type { Manifest } from "./fetchers" 3 | 4 | export interface Icon { 5 | href: string 6 | size: number 7 | touch?: boolean 8 | } 9 | 10 | export interface Head { 11 | manifest?: string 12 | icons: Icon[] 13 | } 14 | 15 | export function parseManifest(manifest: Manifest): Icon[] { 16 | toLog(JSON.stringify(manifest)) 17 | 18 | if (manifest.icons) { 19 | return manifest.icons.map((icon) => ({ 20 | href: icon.src, 21 | size: sizesToNumber(icon.sizes), 22 | })) 23 | } 24 | 25 | return [] 26 | } 27 | 28 | export function parseHead(html: string): Head { 29 | const result: Head = { icons: [] } 30 | const endHeadTag = html.indexOf("") 31 | 32 | const debugLinks: string[] = [] 33 | const debugMetas: string[] = [] 34 | 35 | if (endHeadTag > 0) { 36 | html = html.slice(0, endHeadTag) 37 | } 38 | 39 | if (html.indexOf(" 0) { 40 | html = html 41 | .split(" str.slice(str.indexOf("") + 9)) 43 | .join() 44 | } 45 | 46 | const links = html.split(" `"))}>`) 47 | const metas = html.split(" `"))}>`) 48 | 49 | const sliceAttr = (str = "", from = "", to = "") => { 50 | const start = str.indexOf(from) + from.length 51 | const end = str.indexOf(to, start) + (to.length - 1) 52 | return str.substring(start, end) 53 | } 54 | 55 | for (let meta of metas) { 56 | if (meta.includes("'")) { 57 | meta = meta.replaceAll("'", '"') 58 | } 59 | 60 | const name = sliceAttr(meta, 'name="', '"').toLocaleLowerCase() 61 | const content = sliceAttr(meta, 'content="', '"') 62 | 63 | if (name.includes("apple-touch-icon")) { 64 | result.icons.push({ href: content, size: 100, touch: true }) 65 | } 66 | 67 | debugMetas.push(meta) 68 | } 69 | 70 | for (let link of links) { 71 | if (link.includes("'")) { 72 | link = link.replaceAll("'", '"') 73 | } 74 | 75 | const rel = sliceAttr(link, 'rel="', '"').toLocaleLowerCase() 76 | const href = sliceAttr(link, 'href="', '"') 77 | const sizes = sliceAttr(link, 'sizes="', '"').toLocaleLowerCase() 78 | 79 | if (rel.includes("manifest")) { 80 | result.manifest = href 81 | } 82 | 83 | if (rel.includes("icon")) { 84 | result.icons.push({ 85 | href, 86 | size: sizesToNumber(sizes), 87 | touch: rel.includes("apple-touch") || rel.includes("fluid") || rel.includes("mask"), 88 | }) 89 | } 90 | 91 | debugLinks.push(link) 92 | } 93 | 94 | toDebug("html", html) 95 | toDebug("metas", debugMetas) 96 | toDebug("links", debugLinks) 97 | toDebug("head", result) 98 | 99 | return result 100 | } 101 | -------------------------------------------------------------------------------- /package/icons/amazon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /package/icons/sheets.google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/icons/copilot.microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | 23 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 40px; 3 | margin: 0; 4 | font-family: 5 | system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 6 | color-scheme: light dark; 7 | background-color: #000; 8 | min-height: 100vh; 9 | display: flex; 10 | place-content: center; 11 | place-items: center; 12 | flex-direction: column; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | #previewer { 20 | display: flex; 21 | flex-direction: column; 22 | height: 5.1em; 23 | width: 20em; 24 | color: #ddd; 25 | border-radius: 0.2em; 26 | background-color: #222; 27 | overflow: hidden; 28 | } 29 | 30 | button { 31 | display: inline-block; 32 | padding: 0; 33 | font-size: 1em; 34 | line-height: 1em; 35 | border-radius: 1em; 36 | border: none; 37 | cursor: pointer; 38 | color: inherit; 39 | background-color: transparent; 40 | } 41 | 42 | button span { 43 | vertical-align: text-top; 44 | } 45 | 46 | button:hover { 47 | background-color: #fff2; 48 | } 49 | 50 | button:active { 51 | color: white; 52 | background-color: #fff3; 53 | } 54 | 55 | /* Tab */ 56 | 57 | .tab, 58 | .address-bar { 59 | background-color: #333; 60 | } 61 | 62 | .tab { 63 | width: 16em; 64 | margin-top: 0.5em; 65 | margin-left: 0.5em; 66 | padding: 0.4em 0.6em; 67 | border-top-right-radius: 1em; 68 | border-top-left-radius: 1em; 69 | } 70 | 71 | .tab, 72 | .tab-inner, 73 | .tab-info { 74 | display: flex; 75 | gap: 0.6em; 76 | justify-content: space-between; 77 | align-items: center; 78 | } 79 | 80 | .tab-inner { 81 | width: 100%; 82 | } 83 | 84 | .tab img { 85 | width: 1.6em; 86 | height: 1.6em; 87 | border: none; 88 | border-radius: 0.2em; 89 | transition: opacity 0.02s linear; 90 | cursor: pointer; 91 | } 92 | 93 | .tab img:hover { 94 | opacity: 0.7; 95 | } 96 | 97 | .tab img:active { 98 | opacity: 0.5; 99 | } 100 | 101 | .tab img.loading { 102 | opacity: 0.2; 103 | } 104 | 105 | .tab button { 106 | padding: 0 0.2em; 107 | width: 1.2em; 108 | height: 1.2em; 109 | } 110 | 111 | /* Address bar */ 112 | 113 | .address-bar { 114 | display: flex; 115 | align-items: center; 116 | padding: 0.2em 0 0.2em 0.5em; 117 | gap: 0.5em; 118 | width: 100%; 119 | } 120 | 121 | .address-bar form { 122 | width: 100%; 123 | } 124 | 125 | .address-bar input { 126 | height: 2em; 127 | width: 100%; 128 | color: inherit; 129 | font: inherit; 130 | font-size: 0.9em; 131 | border: none; 132 | border-top-left-radius: 2em; 133 | border-bottom-left-radius: 2em; 134 | outline: 0.1em solid transparent; 135 | padding-inline-start: 1em; 136 | background-color: #2a2a2a; 137 | } 138 | 139 | .address-bar input:focus-visible { 140 | outline-color: cornflowerblue; 141 | } 142 | 143 | .address-bar-controls { 144 | display: flex; 145 | column-gap: 0.3em; 146 | } 147 | 148 | .address-bar-controls button { 149 | font-size: 1.6em; 150 | width: 1.1em; 151 | font-family: monospace; 152 | } 153 | 154 | .address-bar-controls button:disabled { 155 | user-select: none; 156 | touch-action: none; 157 | pointer-events: none; 158 | opacity: 0.6; 159 | } 160 | 161 | /* Infos */ 162 | 163 | #infos { 164 | display: none; 165 | color: #ddd; 166 | font-size: 1rem; 167 | font-family: monospace; 168 | text-align: center; 169 | margin-top: 3em; 170 | } 171 | 172 | /* Zoom modal */ 173 | 174 | #big-icon-modal { 175 | outline: none; 176 | padding: 0; 177 | border: none; 178 | } 179 | 180 | #big-icon-modal::backdrop { 181 | background-color: #000; 182 | } 183 | 184 | #big-icon-modal img { 185 | width: 50vw; 186 | height: 50vw; 187 | max-width: 80vh; 188 | max-height: 80vh; 189 | object-fit: fill; 190 | } 191 | 192 | /* footer */ 193 | 194 | footer { 195 | font-family: monospace; 196 | margin-top: 3em; 197 | text-align: center; 198 | font-size: 1rem; 199 | } 200 | 201 | footer a { 202 | opacity: 0.6; 203 | color: #ddd; 204 | text-decoration: none; 205 | margin: 0 0.6em; 206 | } 207 | 208 | footer a:hover, 209 | footer a:active { 210 | opacity: 1; 211 | text-decoration: underline; 212 | } 213 | 214 | @media screen and (max-width: 860px) { 215 | body { 216 | font-size: 30px; 217 | } 218 | } 219 | 220 | @media screen and (max-width: 620px) { 221 | body { 222 | font-size: 20px; 223 | } 224 | 225 | .address-bar input { 226 | outline-width: 2px; 227 | } 228 | } 229 | 230 | @media screen and (max-width: 500px) { 231 | body { 232 | font-size: 16px; 233 | } 234 | 235 | footer { 236 | font-size: 12px; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /package/icons/openai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/icons/outlook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | Rectangle.308rect307Sheet.88Sheet.86Sheet.87path356 297 | 298 | -------------------------------------------------------------------------------- /package/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getDebug, getIconFromList, initDebug, initLog, sortClosestToSize, toDebug, toLog } from "./helpers" 2 | import { fetchHtml, fetchIcon, fetchManifest } from "./fetchers" 3 | import { parseHead, parseManifest } from "./parsers" 4 | import STATIC_ICONS from "./icons" 5 | 6 | import type { Icon } from "./parsers" 7 | import type { Debug } from "./helpers" 8 | 9 | interface MainOptions { 10 | log?: true 11 | check?: "all" | "best" | "none" 12 | } 13 | 14 | interface FaviconList { 15 | found: string[] 16 | fallbacks: string[] 17 | notfound: string 18 | } 19 | 20 | /** 21 | * Find the best favicon for the specified query. 22 | * 23 | * @param text - Receive a favicon as a string URL 24 | * @param blob - Receive a favicon as a blob (image) 25 | * @param fetch - Accepts a Request with /:type/:url as path 26 | * @param debug - Returns a JSON of all data collected by favicon fetcher 27 | * @param list - Get all favicon URL found for the specified query 28 | * 29 | * @example 30 | * import favicon from "@victr/favicon-fetcher" 31 | * await favicon.text("...") 32 | * 33 | * @example 34 | * import { faviconAsText } from "@victr/favicon-fetcher" 35 | * await faviconAsText("...") 36 | */ 37 | export default { 38 | text: faviconAsText, 39 | blob: faviconAsBlob, 40 | fetch: faviconAsFetch, 41 | debug: debugFavicon, 42 | list: listAvailableFavicons, 43 | } 44 | 45 | /** 46 | * Specify a website, receive a favicon as a string URL 47 | * 48 | * @param query - Must add protocol in order to work (http:// or https://) 49 | * @param options 50 | * @param options.log - Adds console errors 51 | * @param options.check - Either check validity for all found icons, best found icon, or none 52 | * @returns A favicon URL found for the query specified 53 | */ 54 | export async function faviconAsText(query: string, options?: MainOptions): Promise { 55 | return await main(query, "text", options ?? { check: "best" }) 56 | } 57 | 58 | /** 59 | * Specify a website, receive a favicon as a blob (image) 60 | * 61 | * @param query - Must add protocol in order to work (http:// or https://) 62 | * @param options 63 | * @param options.log - Adds console errors 64 | * @param options.check - Either check validity for all found icons, best found icon, or none 65 | * @returns A favicon found for the query specified 66 | */ 67 | export async function faviconAsBlob(query: string, options?: MainOptions): Promise { 68 | return await main(query, "blob", options ?? { check: "best" }) 69 | } 70 | 71 | /** 72 | * Specify a website, receive a list of favicon URLs 73 | * 74 | * @param query - Must add protocol in order to work (http:// or https://) 75 | * @returns All favicon URL found for the query specified 76 | */ 77 | export async function listAvailableFavicons(query: string): Promise { 78 | const list = await createFaviconList(query) 79 | return [...list.found, ...list.fallbacks, list.notfound] 80 | } 81 | 82 | /** 83 | * Similar to list, it logs all steps favicon fetcher retrieved or parsed data 84 | * 85 | * @param query - Must add protocol in order to work (http:// or https://) 86 | * @returns A collection of data parsed by favicon fetcher 87 | */ 88 | export async function debugFavicon(query: string): Promise { 89 | initDebug() 90 | await main(query, "text", { check: "none" }) 91 | return getDebug() 92 | } 93 | 94 | /** 95 | * Request a favicon using the fetch syntax. 96 | * 97 | * @param request A GET request with the return type and query as its pathname 98 | * @returns A response with a 30 days cache control 99 | * @example 100 | * // Get wikipedia's favicon as text 101 | * const url = "http://example.com/text/https://wikipedia.org" 102 | * const resp = await favicon.fetch(url) 103 | * const src = await resp.text() 104 | */ 105 | export async function faviconAsFetch(request: Request): Promise { 106 | const url = new URL(request.url) 107 | const headers = new Headers({ 108 | "Content-Type": "text/plain", 109 | "Access-Control-Allow-Origin": "*", 110 | "Access-Control-Allow-Methods": "GET", 111 | "Cache-Control": "public, max-age=604800, immutable", 112 | }) 113 | 114 | let query: string = "" 115 | let type: string = "" 116 | 117 | if (url.pathname.includes("/blob/")) type = "blob" 118 | if (url.pathname.includes("/text/")) type = "text" 119 | if (url.pathname.includes("/list/")) type = "list" 120 | if (url.pathname.includes("/debug/")) type = "debug" 121 | 122 | query = url.pathname.slice(url.pathname.indexOf(`/${type}/`) + type.length + 2) 123 | 124 | switch (type) { 125 | case "text": { 126 | const text = await faviconAsText(query) 127 | return new Response(text, { headers }) 128 | } 129 | 130 | case "blob": { 131 | const blob = await faviconAsBlob(query) 132 | headers.set("Content-Type", blob.type) 133 | return new Response(blob, { headers }) 134 | } 135 | 136 | case "list": { 137 | const list = await listAvailableFavicons(query) 138 | headers.set("Content-Type", "application/json") 139 | return new Response(JSON.stringify(list), { headers }) 140 | } 141 | 142 | case "debug": { 143 | const debug = await debugFavicon(query) 144 | headers.set("Content-Type", "application/json") 145 | return new Response(JSON.stringify(debug), { headers }) 146 | } 147 | 148 | case "": { 149 | return new Response('Type must be "blob", "text", "list", or "debug"', { 150 | status: 404, 151 | }) 152 | } 153 | 154 | default: { 155 | return new Response("Undefined error", { 156 | status: 500, 157 | }) 158 | } 159 | } 160 | } 161 | 162 | // 163 | // 164 | // 165 | 166 | async function main(query: string, as: "blob", options: MainOptions): Promise 167 | async function main(query: string, as: "text", options: MainOptions): Promise 168 | async function main(query: string, as: "blob" | "text", options: MainOptions) { 169 | initLog(!!options.log) 170 | 171 | const list = await createFaviconList(query) 172 | const isNone = options.check === "none" 173 | const isBest = options.check === "best" 174 | const isAll = options.check === "all" 175 | 176 | // No check 177 | 178 | if (isNone) { 179 | if (as === "text") { 180 | return list.found[0] 181 | } 182 | 183 | if (as === "blob") { 184 | const blob = await fetchIcon(list.found[0]) 185 | return blob ? blob : await fetchIcon(list.notfound) 186 | } 187 | } 188 | 189 | // Checks and fallbacks 190 | 191 | const found = isBest ? [list.found[0]] : isAll ? list.found : [] 192 | const urls = found.concat(list.fallbacks, list.notfound) 193 | 194 | for (const url of urls) { 195 | const blob = await fetchIcon(url) 196 | 197 | if (blob?.type.includes("image")) { 198 | if (as === "text") return url 199 | if (as === "blob") return blob 200 | } 201 | } 202 | 203 | // Nothing found 204 | 205 | throw new Error("No valid icon found in list") 206 | } 207 | 208 | async function createFaviconList(query: string): Promise { 209 | const result: FaviconList = { 210 | found: [], 211 | fallbacks: [], 212 | notfound: `${STATIC_ICONS.HOST}notfound.svg`, 213 | } 214 | 215 | // Step 1: Return not found with bad query 216 | 217 | try { 218 | new URL(query) 219 | } catch (_) { 220 | toLog(query, "Query is invalid") 221 | return result 222 | } 223 | 224 | // Step 2: Is available from static list 225 | 226 | const staticIconUrl = getIconFromList(query) 227 | 228 | if (staticIconUrl) { 229 | result.found = [`${STATIC_ICONS.HOST}${staticIconUrl}`] 230 | return result 231 | } 232 | 233 | // Step 3: Put and sort all potential icon paths in a list 234 | 235 | const icons: Icon[] = [] 236 | const { html, redirected } = await fetchHtml(query) 237 | 238 | if (redirected) { 239 | query = redirected 240 | } 241 | 242 | if (html) { 243 | const head = parseHead(html) 244 | icons.push(...sortClosestToSize(head.icons, 144)) 245 | 246 | if (head.manifest) { 247 | const path = generateFullPath(head.manifest, query) 248 | const manifest = await fetchManifest(path[0]) 249 | 250 | if (manifest) { 251 | const manifestIcons = parseManifest(manifest) 252 | icons.push(...sortClosestToSize(manifestIcons, 144)) 253 | } 254 | } 255 | } 256 | 257 | // 3. bis. Add fallbacks 258 | 259 | const { host, origin } = new URL(query) 260 | const faviconico = `https://${host}/favicon.ico` 261 | const duckduckgo = `https://www.google.com/s2/favicons?domain=${origin}&sz=128` 262 | 263 | result.fallbacks.push(duckduckgo) 264 | result.fallbacks.push(faviconico) 265 | 266 | // Step 4: Add list of href 267 | 268 | for (const icon of icons) { 269 | result.found = result.found.concat(generateFullPath(icon.href, query)) 270 | } 271 | 272 | // Step 5: Return 273 | 274 | toDebug("paths", result) 275 | 276 | return result 277 | } 278 | 279 | function generateFullPath(href: string, query: string): string[] { 280 | // a. Check for always valid paths 281 | 282 | if (href.startsWith("data:image/")) { 283 | return [href] 284 | } 285 | 286 | if (href.startsWith("http")) { 287 | return [href] 288 | } 289 | 290 | if (href.startsWith("//")) { 291 | return [`https:${href}`] 292 | } 293 | 294 | // b. Query sanitation 295 | 296 | try { 297 | new URL(query) 298 | } catch (_error) { 299 | toLog(query, href, "Cannot create a valid URL") 300 | return [] 301 | } 302 | 303 | const url = new URL(query) 304 | let pathname = url.pathname 305 | 306 | if (pathname.endsWith("/")) { 307 | pathname = pathname.slice(0, pathname.length - 2) 308 | } 309 | 310 | // c. Return root and/or relative paths 311 | 312 | if (href.startsWith("/")) { 313 | return [`${url.origin}${href}`] 314 | } 315 | 316 | return [`${url.origin}/${href}`, `${url.origin}${pathname}/${href}`] 317 | } 318 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@std/assert@^1.0.13": "1.0.13", 5 | "jsr:@std/expect@^1.0.16": "1.0.16", 6 | "jsr:@std/internal@^1.0.6": "1.0.9", 7 | "jsr:@std/internal@^1.0.7": "1.0.9", 8 | "npm:tsup@^8.5.0": "8.5.0_typescript@5.8.3_esbuild@0.25.6", 9 | "npm:typescript@^5.8.3": "5.8.3" 10 | }, 11 | "jsr": { 12 | "@std/assert@1.0.13": { 13 | "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 14 | "dependencies": [ 15 | "jsr:@std/internal@^1.0.6" 16 | ] 17 | }, 18 | "@std/expect@1.0.16": { 19 | "integrity": "ceeef6dda21f256a5f0f083fcc0eaca175428b523359a9b1d9b3a1df11cc7391", 20 | "dependencies": [ 21 | "jsr:@std/assert", 22 | "jsr:@std/internal@^1.0.7" 23 | ] 24 | }, 25 | "@std/internal@1.0.9": { 26 | "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" 27 | } 28 | }, 29 | "npm": { 30 | "@esbuild/aix-ppc64@0.25.6": { 31 | "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", 32 | "os": ["aix"], 33 | "cpu": ["ppc64"] 34 | }, 35 | "@esbuild/android-arm64@0.25.6": { 36 | "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", 37 | "os": ["android"], 38 | "cpu": ["arm64"] 39 | }, 40 | "@esbuild/android-arm@0.25.6": { 41 | "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", 42 | "os": ["android"], 43 | "cpu": ["arm"] 44 | }, 45 | "@esbuild/android-x64@0.25.6": { 46 | "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", 47 | "os": ["android"], 48 | "cpu": ["x64"] 49 | }, 50 | "@esbuild/darwin-arm64@0.25.6": { 51 | "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", 52 | "os": ["darwin"], 53 | "cpu": ["arm64"] 54 | }, 55 | "@esbuild/darwin-x64@0.25.6": { 56 | "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", 57 | "os": ["darwin"], 58 | "cpu": ["x64"] 59 | }, 60 | "@esbuild/freebsd-arm64@0.25.6": { 61 | "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", 62 | "os": ["freebsd"], 63 | "cpu": ["arm64"] 64 | }, 65 | "@esbuild/freebsd-x64@0.25.6": { 66 | "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", 67 | "os": ["freebsd"], 68 | "cpu": ["x64"] 69 | }, 70 | "@esbuild/linux-arm64@0.25.6": { 71 | "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", 72 | "os": ["linux"], 73 | "cpu": ["arm64"] 74 | }, 75 | "@esbuild/linux-arm@0.25.6": { 76 | "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", 77 | "os": ["linux"], 78 | "cpu": ["arm"] 79 | }, 80 | "@esbuild/linux-ia32@0.25.6": { 81 | "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", 82 | "os": ["linux"], 83 | "cpu": ["ia32"] 84 | }, 85 | "@esbuild/linux-loong64@0.25.6": { 86 | "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", 87 | "os": ["linux"], 88 | "cpu": ["loong64"] 89 | }, 90 | "@esbuild/linux-mips64el@0.25.6": { 91 | "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", 92 | "os": ["linux"], 93 | "cpu": ["mips64el"] 94 | }, 95 | "@esbuild/linux-ppc64@0.25.6": { 96 | "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", 97 | "os": ["linux"], 98 | "cpu": ["ppc64"] 99 | }, 100 | "@esbuild/linux-riscv64@0.25.6": { 101 | "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", 102 | "os": ["linux"], 103 | "cpu": ["riscv64"] 104 | }, 105 | "@esbuild/linux-s390x@0.25.6": { 106 | "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", 107 | "os": ["linux"], 108 | "cpu": ["s390x"] 109 | }, 110 | "@esbuild/linux-x64@0.25.6": { 111 | "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", 112 | "os": ["linux"], 113 | "cpu": ["x64"] 114 | }, 115 | "@esbuild/netbsd-arm64@0.25.6": { 116 | "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", 117 | "os": ["netbsd"], 118 | "cpu": ["arm64"] 119 | }, 120 | "@esbuild/netbsd-x64@0.25.6": { 121 | "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", 122 | "os": ["netbsd"], 123 | "cpu": ["x64"] 124 | }, 125 | "@esbuild/openbsd-arm64@0.25.6": { 126 | "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", 127 | "os": ["openbsd"], 128 | "cpu": ["arm64"] 129 | }, 130 | "@esbuild/openbsd-x64@0.25.6": { 131 | "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", 132 | "os": ["openbsd"], 133 | "cpu": ["x64"] 134 | }, 135 | "@esbuild/openharmony-arm64@0.25.6": { 136 | "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", 137 | "os": ["openharmony"], 138 | "cpu": ["arm64"] 139 | }, 140 | "@esbuild/sunos-x64@0.25.6": { 141 | "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", 142 | "os": ["sunos"], 143 | "cpu": ["x64"] 144 | }, 145 | "@esbuild/win32-arm64@0.25.6": { 146 | "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", 147 | "os": ["win32"], 148 | "cpu": ["arm64"] 149 | }, 150 | "@esbuild/win32-ia32@0.25.6": { 151 | "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", 152 | "os": ["win32"], 153 | "cpu": ["ia32"] 154 | }, 155 | "@esbuild/win32-x64@0.25.6": { 156 | "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", 157 | "os": ["win32"], 158 | "cpu": ["x64"] 159 | }, 160 | "@isaacs/cliui@8.0.2": { 161 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 162 | "dependencies": [ 163 | "string-width@5.1.2", 164 | "string-width-cjs@npm:string-width@4.2.3", 165 | "strip-ansi@7.1.0", 166 | "strip-ansi-cjs@npm:strip-ansi@6.0.1", 167 | "wrap-ansi@8.1.0", 168 | "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" 169 | ] 170 | }, 171 | "@jridgewell/gen-mapping@0.3.12": { 172 | "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", 173 | "dependencies": [ 174 | "@jridgewell/sourcemap-codec", 175 | "@jridgewell/trace-mapping" 176 | ] 177 | }, 178 | "@jridgewell/resolve-uri@3.1.2": { 179 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" 180 | }, 181 | "@jridgewell/sourcemap-codec@1.5.4": { 182 | "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" 183 | }, 184 | "@jridgewell/trace-mapping@0.3.29": { 185 | "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", 186 | "dependencies": [ 187 | "@jridgewell/resolve-uri", 188 | "@jridgewell/sourcemap-codec" 189 | ] 190 | }, 191 | "@pkgjs/parseargs@0.11.0": { 192 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" 193 | }, 194 | "@rollup/rollup-android-arm-eabi@4.45.0": { 195 | "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", 196 | "os": ["android"], 197 | "cpu": ["arm"] 198 | }, 199 | "@rollup/rollup-android-arm64@4.45.0": { 200 | "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", 201 | "os": ["android"], 202 | "cpu": ["arm64"] 203 | }, 204 | "@rollup/rollup-darwin-arm64@4.45.0": { 205 | "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", 206 | "os": ["darwin"], 207 | "cpu": ["arm64"] 208 | }, 209 | "@rollup/rollup-darwin-x64@4.45.0": { 210 | "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", 211 | "os": ["darwin"], 212 | "cpu": ["x64"] 213 | }, 214 | "@rollup/rollup-freebsd-arm64@4.45.0": { 215 | "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", 216 | "os": ["freebsd"], 217 | "cpu": ["arm64"] 218 | }, 219 | "@rollup/rollup-freebsd-x64@4.45.0": { 220 | "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", 221 | "os": ["freebsd"], 222 | "cpu": ["x64"] 223 | }, 224 | "@rollup/rollup-linux-arm-gnueabihf@4.45.0": { 225 | "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", 226 | "os": ["linux"], 227 | "cpu": ["arm"] 228 | }, 229 | "@rollup/rollup-linux-arm-musleabihf@4.45.0": { 230 | "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", 231 | "os": ["linux"], 232 | "cpu": ["arm"] 233 | }, 234 | "@rollup/rollup-linux-arm64-gnu@4.45.0": { 235 | "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", 236 | "os": ["linux"], 237 | "cpu": ["arm64"] 238 | }, 239 | "@rollup/rollup-linux-arm64-musl@4.45.0": { 240 | "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", 241 | "os": ["linux"], 242 | "cpu": ["arm64"] 243 | }, 244 | "@rollup/rollup-linux-loongarch64-gnu@4.45.0": { 245 | "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", 246 | "os": ["linux"], 247 | "cpu": ["loong64"] 248 | }, 249 | "@rollup/rollup-linux-powerpc64le-gnu@4.45.0": { 250 | "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", 251 | "os": ["linux"], 252 | "cpu": ["ppc64"] 253 | }, 254 | "@rollup/rollup-linux-riscv64-gnu@4.45.0": { 255 | "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", 256 | "os": ["linux"], 257 | "cpu": ["riscv64"] 258 | }, 259 | "@rollup/rollup-linux-riscv64-musl@4.45.0": { 260 | "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", 261 | "os": ["linux"], 262 | "cpu": ["riscv64"] 263 | }, 264 | "@rollup/rollup-linux-s390x-gnu@4.45.0": { 265 | "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", 266 | "os": ["linux"], 267 | "cpu": ["s390x"] 268 | }, 269 | "@rollup/rollup-linux-x64-gnu@4.45.0": { 270 | "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", 271 | "os": ["linux"], 272 | "cpu": ["x64"] 273 | }, 274 | "@rollup/rollup-linux-x64-musl@4.45.0": { 275 | "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", 276 | "os": ["linux"], 277 | "cpu": ["x64"] 278 | }, 279 | "@rollup/rollup-win32-arm64-msvc@4.45.0": { 280 | "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", 281 | "os": ["win32"], 282 | "cpu": ["arm64"] 283 | }, 284 | "@rollup/rollup-win32-ia32-msvc@4.45.0": { 285 | "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", 286 | "os": ["win32"], 287 | "cpu": ["ia32"] 288 | }, 289 | "@rollup/rollup-win32-x64-msvc@4.45.0": { 290 | "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", 291 | "os": ["win32"], 292 | "cpu": ["x64"] 293 | }, 294 | "@types/estree@1.0.8": { 295 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" 296 | }, 297 | "acorn@8.15.0": { 298 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 299 | "bin": true 300 | }, 301 | "ansi-regex@5.0.1": { 302 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 303 | }, 304 | "ansi-regex@6.1.0": { 305 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" 306 | }, 307 | "ansi-styles@4.3.0": { 308 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 309 | "dependencies": [ 310 | "color-convert" 311 | ] 312 | }, 313 | "ansi-styles@6.2.1": { 314 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" 315 | }, 316 | "any-promise@1.3.0": { 317 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" 318 | }, 319 | "balanced-match@1.0.2": { 320 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 321 | }, 322 | "brace-expansion@2.0.2": { 323 | "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 324 | "dependencies": [ 325 | "balanced-match" 326 | ] 327 | }, 328 | "bundle-require@5.1.0_esbuild@0.25.6": { 329 | "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", 330 | "dependencies": [ 331 | "esbuild", 332 | "load-tsconfig" 333 | ] 334 | }, 335 | "cac@6.7.14": { 336 | "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" 337 | }, 338 | "chokidar@4.0.3": { 339 | "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 340 | "dependencies": [ 341 | "readdirp" 342 | ] 343 | }, 344 | "color-convert@2.0.1": { 345 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 346 | "dependencies": [ 347 | "color-name" 348 | ] 349 | }, 350 | "color-name@1.1.4": { 351 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 352 | }, 353 | "commander@4.1.1": { 354 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" 355 | }, 356 | "confbox@0.1.8": { 357 | "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" 358 | }, 359 | "consola@3.4.2": { 360 | "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==" 361 | }, 362 | "cross-spawn@7.0.6": { 363 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 364 | "dependencies": [ 365 | "path-key", 366 | "shebang-command", 367 | "which" 368 | ] 369 | }, 370 | "debug@4.4.1": { 371 | "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 372 | "dependencies": [ 373 | "ms" 374 | ] 375 | }, 376 | "eastasianwidth@0.2.0": { 377 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" 378 | }, 379 | "emoji-regex@8.0.0": { 380 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 381 | }, 382 | "emoji-regex@9.2.2": { 383 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" 384 | }, 385 | "esbuild@0.25.6": { 386 | "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", 387 | "optionalDependencies": [ 388 | "@esbuild/aix-ppc64", 389 | "@esbuild/android-arm", 390 | "@esbuild/android-arm64", 391 | "@esbuild/android-x64", 392 | "@esbuild/darwin-arm64", 393 | "@esbuild/darwin-x64", 394 | "@esbuild/freebsd-arm64", 395 | "@esbuild/freebsd-x64", 396 | "@esbuild/linux-arm", 397 | "@esbuild/linux-arm64", 398 | "@esbuild/linux-ia32", 399 | "@esbuild/linux-loong64", 400 | "@esbuild/linux-mips64el", 401 | "@esbuild/linux-ppc64", 402 | "@esbuild/linux-riscv64", 403 | "@esbuild/linux-s390x", 404 | "@esbuild/linux-x64", 405 | "@esbuild/netbsd-arm64", 406 | "@esbuild/netbsd-x64", 407 | "@esbuild/openbsd-arm64", 408 | "@esbuild/openbsd-x64", 409 | "@esbuild/openharmony-arm64", 410 | "@esbuild/sunos-x64", 411 | "@esbuild/win32-arm64", 412 | "@esbuild/win32-ia32", 413 | "@esbuild/win32-x64" 414 | ], 415 | "scripts": true, 416 | "bin": true 417 | }, 418 | "fdir@6.4.6_picomatch@4.0.2": { 419 | "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", 420 | "dependencies": [ 421 | "picomatch" 422 | ], 423 | "optionalPeers": [ 424 | "picomatch" 425 | ] 426 | }, 427 | "fix-dts-default-cjs-exports@1.0.1": { 428 | "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", 429 | "dependencies": [ 430 | "magic-string", 431 | "mlly", 432 | "rollup" 433 | ] 434 | }, 435 | "foreground-child@3.3.1": { 436 | "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 437 | "dependencies": [ 438 | "cross-spawn", 439 | "signal-exit" 440 | ] 441 | }, 442 | "fsevents@2.3.3": { 443 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 444 | "os": ["darwin"], 445 | "scripts": true 446 | }, 447 | "glob@10.4.5": { 448 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 449 | "dependencies": [ 450 | "foreground-child", 451 | "jackspeak", 452 | "minimatch", 453 | "minipass", 454 | "package-json-from-dist", 455 | "path-scurry" 456 | ], 457 | "bin": true 458 | }, 459 | "is-fullwidth-code-point@3.0.0": { 460 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 461 | }, 462 | "isexe@2.0.0": { 463 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 464 | }, 465 | "jackspeak@3.4.3": { 466 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 467 | "dependencies": [ 468 | "@isaacs/cliui" 469 | ], 470 | "optionalDependencies": [ 471 | "@pkgjs/parseargs" 472 | ] 473 | }, 474 | "joycon@3.1.1": { 475 | "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" 476 | }, 477 | "lilconfig@3.1.3": { 478 | "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" 479 | }, 480 | "lines-and-columns@1.2.4": { 481 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 482 | }, 483 | "load-tsconfig@0.2.5": { 484 | "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==" 485 | }, 486 | "lodash.sortby@4.7.0": { 487 | "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" 488 | }, 489 | "lru-cache@10.4.3": { 490 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" 491 | }, 492 | "magic-string@0.30.17": { 493 | "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 494 | "dependencies": [ 495 | "@jridgewell/sourcemap-codec" 496 | ] 497 | }, 498 | "minimatch@9.0.5": { 499 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 500 | "dependencies": [ 501 | "brace-expansion" 502 | ] 503 | }, 504 | "minipass@7.1.2": { 505 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" 506 | }, 507 | "mlly@1.7.4": { 508 | "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", 509 | "dependencies": [ 510 | "acorn", 511 | "pathe", 512 | "pkg-types", 513 | "ufo" 514 | ] 515 | }, 516 | "ms@2.1.3": { 517 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 518 | }, 519 | "mz@2.7.0": { 520 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 521 | "dependencies": [ 522 | "any-promise", 523 | "object-assign", 524 | "thenify-all" 525 | ] 526 | }, 527 | "object-assign@4.1.1": { 528 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 529 | }, 530 | "package-json-from-dist@1.0.1": { 531 | "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" 532 | }, 533 | "path-key@3.1.1": { 534 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 535 | }, 536 | "path-scurry@1.11.1": { 537 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 538 | "dependencies": [ 539 | "lru-cache", 540 | "minipass" 541 | ] 542 | }, 543 | "pathe@2.0.3": { 544 | "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" 545 | }, 546 | "picocolors@1.1.1": { 547 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 548 | }, 549 | "picomatch@4.0.2": { 550 | "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" 551 | }, 552 | "pirates@4.0.7": { 553 | "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==" 554 | }, 555 | "pkg-types@1.3.1": { 556 | "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", 557 | "dependencies": [ 558 | "confbox", 559 | "mlly", 560 | "pathe" 561 | ] 562 | }, 563 | "postcss-load-config@6.0.1": { 564 | "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", 565 | "dependencies": [ 566 | "lilconfig" 567 | ] 568 | }, 569 | "punycode@2.3.1": { 570 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 571 | }, 572 | "readdirp@4.1.2": { 573 | "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" 574 | }, 575 | "resolve-from@5.0.0": { 576 | "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" 577 | }, 578 | "rollup@4.45.0": { 579 | "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", 580 | "dependencies": [ 581 | "@types/estree" 582 | ], 583 | "optionalDependencies": [ 584 | "@rollup/rollup-android-arm-eabi", 585 | "@rollup/rollup-android-arm64", 586 | "@rollup/rollup-darwin-arm64", 587 | "@rollup/rollup-darwin-x64", 588 | "@rollup/rollup-freebsd-arm64", 589 | "@rollup/rollup-freebsd-x64", 590 | "@rollup/rollup-linux-arm-gnueabihf", 591 | "@rollup/rollup-linux-arm-musleabihf", 592 | "@rollup/rollup-linux-arm64-gnu", 593 | "@rollup/rollup-linux-arm64-musl", 594 | "@rollup/rollup-linux-loongarch64-gnu", 595 | "@rollup/rollup-linux-powerpc64le-gnu", 596 | "@rollup/rollup-linux-riscv64-gnu", 597 | "@rollup/rollup-linux-riscv64-musl", 598 | "@rollup/rollup-linux-s390x-gnu", 599 | "@rollup/rollup-linux-x64-gnu", 600 | "@rollup/rollup-linux-x64-musl", 601 | "@rollup/rollup-win32-arm64-msvc", 602 | "@rollup/rollup-win32-ia32-msvc", 603 | "@rollup/rollup-win32-x64-msvc", 604 | "fsevents" 605 | ], 606 | "bin": true 607 | }, 608 | "shebang-command@2.0.0": { 609 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 610 | "dependencies": [ 611 | "shebang-regex" 612 | ] 613 | }, 614 | "shebang-regex@3.0.0": { 615 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 616 | }, 617 | "signal-exit@4.1.0": { 618 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" 619 | }, 620 | "source-map@0.8.0-beta.0": { 621 | "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", 622 | "dependencies": [ 623 | "whatwg-url" 624 | ] 625 | }, 626 | "string-width@4.2.3": { 627 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 628 | "dependencies": [ 629 | "emoji-regex@8.0.0", 630 | "is-fullwidth-code-point", 631 | "strip-ansi@6.0.1" 632 | ] 633 | }, 634 | "string-width@5.1.2": { 635 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 636 | "dependencies": [ 637 | "eastasianwidth", 638 | "emoji-regex@9.2.2", 639 | "strip-ansi@7.1.0" 640 | ] 641 | }, 642 | "strip-ansi@6.0.1": { 643 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 644 | "dependencies": [ 645 | "ansi-regex@5.0.1" 646 | ] 647 | }, 648 | "strip-ansi@7.1.0": { 649 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 650 | "dependencies": [ 651 | "ansi-regex@6.1.0" 652 | ] 653 | }, 654 | "sucrase@3.35.0": { 655 | "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", 656 | "dependencies": [ 657 | "@jridgewell/gen-mapping", 658 | "commander", 659 | "glob", 660 | "lines-and-columns", 661 | "mz", 662 | "pirates", 663 | "ts-interface-checker" 664 | ], 665 | "bin": true 666 | }, 667 | "thenify-all@1.6.0": { 668 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 669 | "dependencies": [ 670 | "thenify" 671 | ] 672 | }, 673 | "thenify@3.3.1": { 674 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 675 | "dependencies": [ 676 | "any-promise" 677 | ] 678 | }, 679 | "tinyexec@0.3.2": { 680 | "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" 681 | }, 682 | "tinyglobby@0.2.14_picomatch@4.0.2": { 683 | "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", 684 | "dependencies": [ 685 | "fdir", 686 | "picomatch" 687 | ] 688 | }, 689 | "tr46@1.0.1": { 690 | "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", 691 | "dependencies": [ 692 | "punycode" 693 | ] 694 | }, 695 | "tree-kill@1.2.2": { 696 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 697 | "bin": true 698 | }, 699 | "ts-interface-checker@0.1.13": { 700 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" 701 | }, 702 | "tsup@8.5.0_typescript@5.8.3_esbuild@0.25.6": { 703 | "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", 704 | "dependencies": [ 705 | "bundle-require", 706 | "cac", 707 | "chokidar", 708 | "consola", 709 | "debug", 710 | "esbuild", 711 | "fix-dts-default-cjs-exports", 712 | "joycon", 713 | "picocolors", 714 | "postcss-load-config", 715 | "resolve-from", 716 | "rollup", 717 | "source-map", 718 | "sucrase", 719 | "tinyexec", 720 | "tinyglobby", 721 | "tree-kill", 722 | "typescript" 723 | ], 724 | "optionalPeers": [ 725 | "typescript" 726 | ], 727 | "bin": true 728 | }, 729 | "typescript@5.8.3": { 730 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 731 | "bin": true 732 | }, 733 | "ufo@1.6.1": { 734 | "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==" 735 | }, 736 | "webidl-conversions@4.0.2": { 737 | "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" 738 | }, 739 | "whatwg-url@7.1.0": { 740 | "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", 741 | "dependencies": [ 742 | "lodash.sortby", 743 | "tr46", 744 | "webidl-conversions" 745 | ] 746 | }, 747 | "which@2.0.2": { 748 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 749 | "dependencies": [ 750 | "isexe" 751 | ], 752 | "bin": true 753 | }, 754 | "wrap-ansi@7.0.0": { 755 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 756 | "dependencies": [ 757 | "ansi-styles@4.3.0", 758 | "string-width@4.2.3", 759 | "strip-ansi@6.0.1" 760 | ] 761 | }, 762 | "wrap-ansi@8.1.0": { 763 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 764 | "dependencies": [ 765 | "ansi-styles@6.2.1", 766 | "string-width@5.1.2", 767 | "strip-ansi@7.1.0" 768 | ] 769 | } 770 | }, 771 | "workspace": { 772 | "dependencies": [ 773 | "jsr:@std/expect@^1.0.16" 774 | ], 775 | "packageJson": { 776 | "dependencies": [ 777 | "npm:tsup@^8.5.0", 778 | "npm:typescript@^5.8.3" 779 | ] 780 | } 781 | } 782 | } 783 | --------------------------------------------------------------------------------