diff --git a/.changeset/fifty-drinks-bake.md b/.changeset/fifty-drinks-bake.md new file mode 100644 index 0000000..5afcd56 --- /dev/null +++ b/.changeset/fifty-drinks-bake.md @@ -0,0 +1,5 @@ +--- +'@chialab/sveltekit-utils': patch +--- + +Fix headers not being written to logs on fetch failures. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c022a4f..0110d0b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,18 @@ jobs: with: cache: yarn + - name: Get Yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - name: Setup Yarn cache + uses: actions/cache@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install project dependencies run: yarn install diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ae15db1..d5511c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,11 @@ jobs: uses: ./.github/workflows/lint.yml secrets: inherit + test: + uses: ./.github/workflows/test.yml + secrets: inherit + release: - needs: lint + needs: [lint, test] uses: ./.github/workflows/release.yml secrets: inherit diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b856dea..473f9c7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,3 +10,7 @@ jobs: lint: uses: ./.github/workflows/lint.yml secrets: inherit + + test: + uses: ./.github/workflows/test.yml + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9d7b05f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test +on: + workflow_call: + +jobs: + unit: + name: Run unit tests + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + cache: yarn + + - name: Get Yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - name: Setup Yarn cache + uses: actions/cache@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install project dependencies + run: yarn install + + - name: Setup Playwright cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright + + - name: Install Playwright browsers + run: yarn run playwright install --with-deps + + - name: Check + run: yarn run test:unit:coverage + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 8d40638..c98bd3a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules /.env.production /.env.*.local /*.log +/tests/coverage/ vite.config.js.timestamp-* vite.config.ts.timestamp-* *.tsbuildinfo diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 51aa879..444596f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,7 @@ "dbaeumer.vscode-eslint", // ESLint official extension "esbenp.prettier-vscode", // Prettier official extension "stylelint.vscode-stylelint", // Stylelint official extension - "svelte.svelte-vscode" // Svelte official extension + "svelte.svelte-vscode", // Svelte official extension + "vitest.explorer" // Vitest official extension ] } diff --git a/package.json b/package.json index 2826df1..337989b 100644 --- a/package.json +++ b/package.json @@ -10,22 +10,22 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "default": "./dist/index.js", - "svelte": "./dist/index.js" + "svelte": "./dist/index.js", + "default": "./dist/index.js" }, "./server": { "types": "./dist/server/index.d.ts", "node": "./dist/server/index.js", - "default": null, - "svelte": null + "svelte": null, + "default": null }, "./logger": { "types": "./dist/logger.d.ts", "default": "./dist/logger.js" }, "./utils": { - "types": "./dist/utils.d.ts", - "default": "./dist/utils.js" + "types": "./dist/utils/index.d.ts", + "default": "./dist/utils/index.js" } }, "scripts": { @@ -34,13 +34,17 @@ "build": "svelte-kit sync && svelte-package", "app:build": "vite build", "app:preview-build": "vite preview | pino-pretty", + "test:unit": "vitest run", + "test:unit:coverage": "vitest run --coverage", + "test:unit:watch": "vitest watch", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "eslint-check": "eslint --ignore-path .gitignore . --ext .js,.cjs,.ts,.svelte", "eslint-fix": "eslint --fix --ignore-path .gitignore . --ext .js,.cjs,.ts,.svelte", "prettier-check": "prettier --check \"./**/*.{json,css,js,cjs,ts,svelte}\"", "prettier-fix": "prettier --write \"./**/*.{json,css,js,cjs,ts,svelte}\"", - "lint-fix-all": "yarn eslint-fix && yarn prettier-fix" + "lint": "yarn run check && yarn run eslint-check && yarn run prettier-fix", + "lint-fix-all": "yarn run eslint-fix && yarn run prettier-fix" }, "dependencies": { "cookie": "^1.0.2", @@ -57,17 +61,21 @@ "@types/node": "^22.9.1", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", + "@vitest/browser": "^3.0.5", + "@vitest/coverage-v8": "^3.0.5", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.46.0", "pino-pretty": "^13.0.0", + "playwright": "^1.50.1", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.3.1", "svelte": "^5.0.0", "svelte-check": "^4.1.4", "tslib": "^2.8.1", "typescript": "^5.6.3", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^3.0.5" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0", diff --git a/src/lib/index.ts b/src/lib/index.ts index fdf2a1d..e6171b3 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,2 @@ export * from './logger.js'; -export * from './url.js'; -export * from './utils.js'; +export * from './utils/index.js'; diff --git a/src/lib/logger.ts b/src/lib/logger.ts index e3415ab..36f0f4a 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,7 +1,7 @@ import * as Pino from 'pino'; import { dev } from '$app/environment'; -const pino: typeof Pino.pino = typeof Pino === 'function' ? (Pino as any) : Pino.default; +const pino: typeof Pino.pino = typeof Pino === 'function' ? Pino : Pino.default; export const logger: Pino.Logger = pino({ transport: dev ? { target: 'pino-pretty' } : undefined, level: dev ? 'debug' : 'info', diff --git a/src/lib/server/cache/base.ts b/src/lib/server/cache/base.ts index 6bae85b..c829252 100644 --- a/src/lib/server/cache/base.ts +++ b/src/lib/server/cache/base.ts @@ -1,4 +1,4 @@ -import type { JitterFn, JitterMode } from '../../utils.js'; +import type { JitterFn, JitterMode } from '../../utils/misc.js'; /** * Base class for caching. diff --git a/src/lib/server/cache/in-memory.ts b/src/lib/server/cache/in-memory.ts index 5503fd4..0c71689 100644 --- a/src/lib/server/cache/in-memory.ts +++ b/src/lib/server/cache/in-memory.ts @@ -1,4 +1,4 @@ -import { createJitter, JitterMode, type JitterFn } from '../../utils.js'; +import { createJitter, JitterMode, type JitterFn } from '../../utils/misc.js'; import { BaseCache } from './base.js'; type ValueWrapper = { value: T; expire: number | undefined }; diff --git a/src/lib/server/cache/redis.ts b/src/lib/server/cache/redis.ts index a0a70e9..2aba710 100644 --- a/src/lib/server/cache/redis.ts +++ b/src/lib/server/cache/redis.ts @@ -9,7 +9,7 @@ import { } from 'redis'; import { logger } from '../../logger.js'; import { BaseCache } from './base.js'; -import { createJitter, JitterMode, type JitterFn } from '../../utils.js'; +import { createJitter, JitterMode, type JitterFn } from '../../utils/misc.js'; type RedisCacheOptions = { keyPrefix?: string; diff --git a/src/lib/server/utils.ts b/src/lib/server/utils.ts index 302cfe1..31207d0 100644 --- a/src/lib/server/utils.ts +++ b/src/lib/server/utils.ts @@ -23,9 +23,4 @@ export type Hashed = { hash: string } & T; * @param input String to compute hash for. * @param algo Algorithm. */ -export const computeHash = (input: string, algo = 'sha256'): string => { - const hash = createHash(algo); - hash.update(input); - - return hash.digest('hex'); -}; +export const computeHash = (input: string, algo = 'sha256'): string => createHash(algo).update(input).digest('hex'); diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index 38388b7..0000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { error } from '@sveltejs/kit'; -import { readonly, writable, type Readable } from 'svelte/store'; -import { logger } from './logger.js'; - -type MaybeFactory = T | (() => T); - -export type HttpsUrl = `https://${string}`; -export type ArrayItem = Readonly extends readonly (infer U)[] ? U : never; - -/** - * Handle errors from API requests. - * @param additionalInfo Additional info that should be logged and might help with contextualizing the error. - * @example - * ```ts - * const myFolder = await fetch('my-folder') - * .catch(handleFetchError({ api: 'my-folder' })); - * ``` - */ -export const handleFetchError = - (additionalInfo: Record = {}) => - (err: unknown): never => { - if (!(err instanceof Error && err.cause instanceof Response)) { - logger.error({ err, ...additionalInfo }, 'Unknown error'); - - throw err; - } - - const status = err.cause.status; - const headers = { ...err.cause.headers }; - if (status === 404) { - logger.warn({ status, headers, ...additionalInfo }, 'Resource not found'); - - error(404); - } - - logger.error({ status, headers, ...additionalInfo }, 'Unexpected fetch error'); - - throw err; - }; - -/** - * Return a Promise that resolves after exactly the requested timeout. - * - * @param ms Timeout in milliseconds after which Promise resolves. - * @param result The value the Promise will resolve to. - * @example - * ```ts - * doSomething(); - * await timeout(1500); // Sleep for 1.5s - * doSomethingElse(); - * ``` - * @example - * ```ts - * const taskTimedOut = Symbol(); - * const { signal, abort } = new AbortController(); - * const result = await Promise.race([ - * fetch(someUrl, { signal }), - * timeout(10_000, taskTimedOut), - * ]); - * if (result === taskTimedOut) { - * abort(); - * - * throw new Error('Fetch took more than 10s to complete, aborting'); - * } - * ``` - */ -export const timeout: { - (ms: number): Promise; - (ms: number, result: T): Promise; -} = (ms: number, result?: T): Promise => new Promise((resolve) => setTimeout(() => resolve(result as T), ms)); - -/** - * Provide user-friendly asynchronous loading for long-running promises. - * - * @param dfd Promise being waited. - * @param showLoaderTimeout Time after which the loader should start showing. - * @param hideLoaderTimeout Minimum time to display loader for after it started showing. - * @returns An object with two keys: - * - `result`: a Promise that resolves as soon as `dfd` is resolved if `showLoaderTimeout` hasn't elapsed yet. - * After that time has elapsed, it won't resolve for at least `hideLoaderTimeout` ms. - * - `showLoader`: a Svelte readable store to display loader. - * @example - * ```svelte - * - * - * {#await result} - * - * {#if $showLoader} - * Loading... - * {/if} - * {:then theResult} - * Done! (display something useful here) - * {:catch err} - * Duh! It failed… - * {/await} - * ``` - */ -export const lazyLoad = ( - dfd: Promise, - showLoaderTimeout = 300, - hideLoaderTimeout = 700, -): { result: Promise; showLoader: Readable } => { - const showLoader = writable(false); - const tookTooLong = Symbol('loading took too long, displaying loader'); - - const result = Promise.race([dfd, timeout(showLoaderTimeout, tookTooLong)]).then((result) => { - if (result !== tookTooLong) { - return result; - } - - showLoader.set(true); - - return Promise.all([dfd, timeout(hideLoaderTimeout, undefined)]).then(([result]) => { - showLoader.set(false); - - return result; - }); - }); - - return { showLoader: readonly(showLoader), result }; -}; - -/** - * Get required parameters from a Map-like structure. - * - * @param params Map-like structure where parameters can be `get()` from. - * @param requiredParams List of required parameters. - * @example - * ```ts - * import { getRequiredParams } from '@chialab/sveltekit-utils'; - * - * const { foo, bar } = getRequiredParams(url.searchParams, ['foo', 'bar']); - * ``` - */ -export const getRequiredParams = ( - params: { get(name: T): string | null | undefined }, - requiredParams: readonly T[], -): Record => { - const extracted = requiredParams.reduce>>( - (store, name) => ({ - ...store, - [name]: params.get(name), - }), - {}, - ); - const missingParams = requiredParams.filter((name) => !extracted[name]); - if (missingParams.length) { - error(400, `The following mandatory parameters are missing or empty: ${missingParams.join(', ')}`); - } - - return extracted as Record; -}; - -/** - * Sanitize HTML string by removing all tags and returning only text content. - */ -export const sanitizeHtml = (html: string | null | undefined): string => { - const div = document.createElement('div'); - div.innerHTML = html ?? ''; - - return div.innerText; -}; - -export enum JitterMode { - /** Do not apply jitter. */ - None = 'none', - /** Full jitter: given _x_, a random value in the range _[0, x]_. */ - Full = 'full', - /** Equal jitter: given _x_, a random value in the range _[x/2, x]_. */ - Equal = 'equal', -} -export type JitterFn = (value: number) => number; -const jitter = { - [JitterMode.None]: (value) => value, - [JitterMode.Full]: (value) => Math.random() * value, - [JitterMode.Equal]: (value) => ((1 + Math.random()) * value) / 2, -} as const satisfies Record; - -/** - * Create jitter function. - * - * @param mode Jitter mode, or custom jitter function. - */ -export const createJitter = (mode: JitterFn | JitterMode): JitterFn => - typeof mode === 'function' ? mode : (jitter[mode] ?? jitter[JitterMode.None]); - -/** - * Call a function and retry until it returns a value or the maximum number of attempts is reached, using "exponential backoff". - * - * @param factory Function to call. - * @param baseMs Base delay in milliseconds. - * @param capMs Maximum delay in milliseconds. - * @param maxAttempts Maximum number of attempts. - * @param jitterMode Jitter mode, or custom jitter function. By default, full jitter is used. - */ -export const backoffRetry = async ( - factory: () => T | undefined | PromiseLike, - baseMs = 500, - capMs = 10_000, - maxAttempts: number = Infinity, - jitterMode: JitterMode | JitterFn = JitterMode.Full, -): Promise | undefined> => { - const jitter = createJitter(jitterMode); - const sleep = (attempt: number) => jitter(Math.min(baseMs * 2 ** attempt, capMs)); - - let attempt = 0; - // eslint-disable-next-line no-constant-condition - while (true) { - const value = await factory(); - if (value !== undefined) { - return value; - } - - attempt++; - if (attempt >= maxAttempts) { - break; - } - - await timeout(sleep(attempt)); - } - - return undefined; -}; - -/** Identity function: checks if its parameter are identity-equal. */ -const identity = (a: unknown, b: unknown) => a === b; - -/** - * Group an array of items using a property returned by the callback. - * - * @param items Items. - * @param cb Callback used to group items together. It should return a "key" or an array of keys to assign the same item to multiple groups. - * @param cmp Comparison function, used when key is not a scalar object thus comparison by strict equality would fail. - * @example - * ```ts - * const groupedContents = group( - * myContents, - * (content) => content.someProperty ?? 'default' - * ); - * // groupedContents = [{ key: 'foo', items: [content1, content2] }, { key: 'default', items: [content2, content4] }] - * - * const byCategory = group( - * folder.children ?? [], - * (obj) => obj.categories ?? [], - * (a, b) => a.name === b.name, - * ); - * // byCategory = [ - * // { key: { name: 'cat-a', label: 'Category A' }, items: [obj1, obj2] }, - * // { key: { name: 'cat-b', label: 'Category B' }, items: [obj1, obj3] }, - * // ] - * ``` - */ -export const group = ( - items: T[], - cb: (item: T) => K | K[], - cmp: (a: K, b: K) => boolean = identity, -): { key: K; items: T[] }[] => - items.reduce<{ key: K; items: T[] }[]>((grouped, item) => { - const keys = cb(item); - for (const key of Array.isArray(keys) ? keys : [keys]) { - const group = grouped.find((group) => cmp(group.key, key)); - if (!group) { - grouped.push({ key, items: [item] }); - } else { - group.items.push(item); - } - } - - return grouped; - }, []); - -/** - * Execute multiple jobs in parallel with limited concurrency. This may be useful for fetching multiple resources without - * flooding the server with an uncontrolled number of simultaneous requests. - * - * @param jobs Iterable of jobs. Ensure the promises are created just when the next iterable element is requested, or this is useless! - * @param concurrency Number of jobs to maintain in pool. - * @example - * ```ts - * // Fetch 30 pages, but keep number of simultaneous requests to 5 maximum. - * const fetchPages = function* (): Iterable> { - * for (let i = 1; i <= 30; i++) { - * yield fetch(`/page/${i}`); - * } - * }; - * for await (const page of asyncIterablePool(fetchPages(), 5)) { - * // Do stuff. - * } - * ``` - * @example - * ```ts - * // Fetch 30 pages, but keep number of simultaneous requests to 5 maximum. - * const fetchPagesFactory = (): (() => Promise)[] => - * [...Array(30)].map((_, i) => () => fetch(`/page/${i + 1}`)); - * for await (const page of asyncIterablePool(fetchPages(), 5)) { - * // Do stuff. - * } - * - * // IMPORTANT: Note that this function makes all `fetch` start immediately once the function is called. - * // This makes the `asyncIterablePool` useless as all the promises have already started! - * // Either use an iterator AND MAKE SURE WORK STARTS ONLY WHEN THE NEXT ELEMENT IS REQUESTED - * // (i.e. careful of `yield*`), or return an array of functions (factories) instead. - * const fetchPages = (): Promise[] => [...Array(30)].map((_, i) => fetch(`/page/${i + 1}`)); - * ``` - */ -export const asyncIterablePool = async function* ( - jobs: Iterable>>, - concurrency = 3, -): AsyncIterable { - const pool = new Map>(); - - const first = () => - Promise.race(pool.values()).then(({ sym, val }) => { - pool.delete(sym); - - return val; - }); - - for (const job of jobs) { - const sym = Symbol('job'); - const startedJob = typeof job === 'function' ? job() : job; - pool.set( - sym, - startedJob.then((val) => ({ sym, val })), - ); - - if (pool.size >= concurrency) { - yield await first(); - } - } - - while (pool.size) { - yield await first(); - } -}; - -/** - * Map an (async) iterator with a function. Same as {@see Array.prototype.map()} but for (async) iterators. - * @param it Iterable. - * @param map Mapping function. - * @example - * ```ts - * import { timeout, mapIterator } from '@chialab/sveltekit-utils'; - * - * function* myIt() { - * yield 1; - * yield 2; - * yield 3; - * } - * async function* myAsyncIt() { - * yield 1; - * await timeout(100); - * yield 2; - * yield await timeout(200, 3); - * } - * const addItsIndex = mapIterator( - * myIt(), // Can also be an async iterator: myAsyncIt(), - * (value, idx) => value + index, // Can also be an async function: (value, idx) => timeout(100, value + index), - * ); - * for await (const val of addItsIndex) { - * console.log(val); // 1, 3, 5 - * } - * ``` - */ -export const mapIterator = async function* ( - it: AsyncIterable | Iterable, - map: (value: T, index: number) => K | PromiseLike, -): AsyncGenerator { - let index = 0; - for await (const val of it) { - yield await map(val, index); - index++; - } - - return index; -}; - -/** - * Get a random integer between two values. - */ -export const getRandomInt = (min: number, max: number): number => - Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min)) + Math.ceil(min)); - -/** - * Check if a value is a Promise or Promise-like (i.e. has a `then()` method). - */ -export const isPromiseLike = (value: unknown): value is PromiseLike => - !!value && typeof (value as { then?: unknown }).then === 'function'; - -/** - * Check if is a mobile device. - */ -export const isMobile = (): boolean => - // @ts-expect-error User Agent Data APIs are experimental. - navigator.userAgentData?.mobile ?? - (/Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/i.test(navigator.userAgent) || - 'ontouchstart' in window || - navigator.maxTouchPoints > 0); - -/** Like {@see Object.entries}, but with less type pain. */ -export const entries = [0]>(obj: T) => - Object.entries(obj) as [keyof T & string, T[keyof T & string]][]; - -/** - * Remove all falsy values from an array and get rid of falsy types from the result of a `filter(Boolean)` call. - * @param arr the array to be sifted. - * @returns the sifted array. - */ -export const sift = (arr: readonly T[]) => arr.filter(Boolean) as NonNullable[]; diff --git a/src/lib/utils/browser.ts b/src/lib/utils/browser.ts new file mode 100644 index 0000000..40d2627 --- /dev/null +++ b/src/lib/utils/browser.ts @@ -0,0 +1,19 @@ +/** + * Sanitize HTML string by removing all tags and returning only text content. + */ +export const sanitizeHtml = (html: string | null | undefined): string => { + const div = document.createElement('div'); + div.innerHTML = html ?? ''; + + return div.innerText; +}; + +/** + * Check if is a mobile device. + */ +export const isMobile = (): boolean => + // @ts-expect-error User Agent Data APIs are experimental. + navigator.userAgentData?.mobile ?? + (/Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/i.test(navigator.userAgent) || + 'ontouchstart' in window || + navigator.maxTouchPoints > 0); diff --git a/src/lib/utils/collections.ts b/src/lib/utils/collections.ts new file mode 100644 index 0000000..c4bcda5 --- /dev/null +++ b/src/lib/utils/collections.ts @@ -0,0 +1,63 @@ +/** + * Infer type of items in an array. + * @deprecated Use `T[number]` instead. + */ +export type ArrayItem = Readonly extends readonly (infer U)[] ? U : never; + +/** Identity function: checks if its parameter are identity-equal. */ +const identity = (a: unknown, b: unknown) => a === b; + +/** + * Group an array of items using a property returned by the callback. + * + * @param items Items. + * @param cb Callback used to group items together. It should return a "key" or an array of keys to assign the same item to multiple groups. + * @param cmp Comparison function, used when key is not a scalar object thus comparison by strict equality would fail. + * @example + * ```ts + * const groupedContents = group( + * myContents, + * (content) => content.someProperty ?? 'default' + * ); + * // groupedContents = [{ key: 'foo', items: [content1, content2] }, { key: 'default', items: [content2, content4] }] + * + * const byCategory = group( + * folder.children ?? [], + * (obj) => obj.categories ?? [], + * (a, b) => a.name === b.name, + * ); + * // byCategory = [ + * // { key: { name: 'cat-a', label: 'Category A' }, items: [obj1, obj2] }, + * // { key: { name: 'cat-b', label: 'Category B' }, items: [obj1, obj3] }, + * // ] + * ``` + */ +export const group = ( + items: T[], + cb: (item: T) => K | K[], + cmp: (a: K, b: K) => boolean = identity, +): { key: K; items: T[] }[] => + items.reduce<{ key: K; items: T[] }[]>((grouped, item) => { + const keys = cb(item); + for (const key of Array.isArray(keys) ? keys : [keys]) { + const group = grouped.find((group) => cmp(group.key, key)); + if (!group) { + grouped.push({ key, items: [item] }); + } else { + group.items.push(item); + } + } + + return grouped; + }, []); + +/** Like {@see Object.entries}, but with less type pain. */ +export const entries = [0]>(obj: T) => + Object.entries(obj) as [keyof T & string, T[keyof T & string]][]; + +/** + * Remove all falsy values from an array and get rid of falsy types from the result of a `filter(Boolean)` call. + * @param arr the array to be sifted. + * @returns the sifted array. + */ +export const sift = (arr: readonly T[]) => arr.filter(Boolean) as NonNullable[]; diff --git a/src/lib/utils/fetch.ts b/src/lib/utils/fetch.ts new file mode 100644 index 0000000..5da8ac0 --- /dev/null +++ b/src/lib/utils/fetch.ts @@ -0,0 +1,34 @@ +import { error } from '@sveltejs/kit'; +import type { Logger } from 'pino'; +import { logger as defaultLogger } from '../logger.js'; + +/** + * Handle errors from API requests. + * @param additionalInfo Additional info that should be logged and might help with contextualizing the error. + * @example + * ```ts + * const myFolder = await fetch('my-folder') + * .catch(handleFetchError({ api: 'my-folder' })); + * ``` + */ +export const handleFetchError = + (additionalInfo: Record = {}, logger: Logger = defaultLogger) => + (err: unknown): never => { + if (!(err instanceof Error && err.cause instanceof Response)) { + logger.error({ err, ...additionalInfo }, 'Unknown error'); + + throw err; + } + + const status = err.cause.status; + const headers = Object.fromEntries(err.cause.headers.entries()); + if (status === 404) { + logger.warn({ status, headers, ...additionalInfo }, 'Resource not found'); + + error(404); + } + + logger.error({ status, headers, ...additionalInfo }, 'Unexpected fetch error'); + + throw err; + }; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..65d80d8 --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,5 @@ +export * from './browser.js'; +export * from './collections.js'; +export * from './misc.js'; +export * from './promises.js'; +export * from './url.js'; diff --git a/src/lib/utils/misc.ts b/src/lib/utils/misc.ts new file mode 100644 index 0000000..7b3af8e --- /dev/null +++ b/src/lib/utils/misc.ts @@ -0,0 +1,97 @@ +/** + * Return a Promise that resolves after exactly the requested timeout. + * + * @param ms Timeout in milliseconds after which Promise resolves. + * @param result The value the Promise will resolve to. + * @example + * ```ts + * doSomething(); + * await timeout(1500); // Sleep for 1.5s + * doSomethingElse(); + * ``` + * @example + * ```ts + * const taskTimedOut = Symbol(); + * const { signal, abort } = new AbortController(); + * const result = await Promise.race([ + * fetch(someUrl, { signal }), + * timeout(10_000, taskTimedOut), + * ]); + * if (result === taskTimedOut) { + * abort(); + * + * throw new Error('Fetch took more than 10s to complete, aborting'); + * } + * ``` + */ +export const timeout: { + (ms: number): Promise; + (ms: number, result: T): Promise; +} = (ms: number, result?: T): Promise => new Promise((resolve) => setTimeout(() => resolve(result as T), ms)); + +/** + * Get a random integer between two values. + */ +export const getRandomInt = (min: number, max: number): number => + Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min)) + Math.ceil(min)); + +export enum JitterMode { + /** Do not apply jitter. */ + None = 'none', + /** Full jitter: given _x_, a random value in the range _[0, x]_. */ + Full = 'full', + /** Equal jitter: given _x_, a random value in the range _[x/2, x]_. */ + Equal = 'equal', +} +export type JitterFn = (value: number) => number; +const jitter = { + [JitterMode.None]: (value) => value, + [JitterMode.Full]: (value) => Math.random() * value, + [JitterMode.Equal]: (value) => ((1 + Math.random()) * value) / 2, +} as const satisfies Record; + +/** + * Create jitter function. + * + * @param mode Jitter mode, or custom jitter function. + */ +export const createJitter = (mode: JitterFn | JitterMode): JitterFn => + typeof mode === 'function' ? mode : (jitter[mode] ?? jitter[JitterMode.None]); + +/** + * Call a function and retry until it returns a value or the maximum number of attempts is reached, using "exponential backoff". + * + * @param factory Function to call. + * @param baseMs Base delay in milliseconds. + * @param capMs Maximum delay in milliseconds. + * @param maxAttempts Maximum number of attempts. + * @param jitterMode Jitter mode, or custom jitter function. By default, full jitter is used. + */ +export const backoffRetry = async ( + factory: () => T | undefined | PromiseLike, + baseMs = 500, + capMs = 10_000, + maxAttempts: number = Infinity, + jitterMode: JitterMode | JitterFn = JitterMode.Full, +): Promise | undefined> => { + const jitter = createJitter(jitterMode); + const sleep = (attempt: number) => jitter(Math.min(baseMs * 2 ** attempt, capMs)); + + let attempt = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const value = await factory(); + if (value !== undefined) { + return value; + } + + attempt++; + if (attempt >= maxAttempts) { + break; + } + + await timeout(sleep(attempt - 1)); + } + + return undefined; +}; diff --git a/src/lib/utils/promises.ts b/src/lib/utils/promises.ts new file mode 100644 index 0000000..348c9e2 --- /dev/null +++ b/src/lib/utils/promises.ts @@ -0,0 +1,172 @@ +import { readonly, writable, type Readable } from 'svelte/store'; +import { timeout } from './misc.js'; + +type MaybeFactory = T | (() => T); + +/** + * Check if a value is a Promise or Promise-like (i.e. has a `then()` method). + */ +export const isPromiseLike = (value: unknown): value is PromiseLike => + !!value && typeof (value as { then?: unknown }).then === 'function'; + +/** + * Provide user-friendly asynchronous loading for long-running promises. + * + * @param dfd Promise being waited. + * @param showLoaderTimeout Time after which the loader should start showing. + * @param hideLoaderTimeout Minimum time to display loader for after it started showing. + * @returns An object with two keys: + * - `result`: a Promise that resolves as soon as `dfd` is resolved if `showLoaderTimeout` hasn't elapsed yet. + * After that time has elapsed, it won't resolve for at least `hideLoaderTimeout` ms. + * - `showLoader`: a Svelte readable store to display loader. + * @example + * ```svelte + * + * + * {#await result} + * + * {#if $showLoader} + * Loading... + * {/if} + * {:then theResult} + * Done! (display something useful here) + * {:catch err} + * Duh! It failed… + * {/await} + * ``` + */ +export const lazyLoad = ( + dfd: Promise, + showLoaderTimeout = 300, + hideLoaderTimeout = 700, +): { result: Promise; showLoader: Readable } => { + const showLoader = writable(false); + const tookTooLong = Symbol('loading took too long, displaying loader'); + + const result = Promise.race([dfd, timeout(showLoaderTimeout, tookTooLong)]).then((result) => { + if (result !== tookTooLong) { + return result; + } + + showLoader.set(true); + + return Promise.all([dfd, timeout(hideLoaderTimeout, undefined)]).then(([result]) => { + showLoader.set(false); + + return result; + }); + }); + + return { showLoader: readonly(showLoader), result }; +}; + +/** + * Execute multiple jobs in parallel with limited concurrency. This may be useful for fetching multiple resources without + * flooding the server with an uncontrolled number of simultaneous requests. + * + * @param jobs Iterable of jobs. Ensure the promises are created just when the next iterable element is requested, or this is useless! + * @param concurrency Number of jobs to maintain in pool. + * @example + * ```ts + * // Fetch 30 pages, but keep number of simultaneous requests to 5 maximum. + * const fetchPages = function* (): Iterable> { + * for (let i = 1; i <= 30; i++) { + * yield fetch(`/page/${i}`); + * } + * }; + * for await (const page of asyncIterablePool(fetchPages(), 5)) { + * // Do stuff. + * } + * ``` + * @example + * ```ts + * // Fetch 30 pages, but keep number of simultaneous requests to 5 maximum. + * const fetchPagesFactory = (): (() => Promise)[] => + * [...Array(30)].map((_, i) => () => fetch(`/page/${i + 1}`)); + * for await (const page of asyncIterablePool(fetchPages(), 5)) { + * // Do stuff. + * } + * + * // IMPORTANT: Note that this function makes all `fetch` start immediately once the function is called. + * // This makes the `asyncIterablePool` useless as all the promises have already started! + * // Either use an iterator AND MAKE SURE WORK STARTS ONLY WHEN THE NEXT ELEMENT IS REQUESTED + * // (i.e. careful of `yield*`), or return an array of functions (factories) instead. + * const fetchPages = (): Promise[] => [...Array(30)].map((_, i) => fetch(`/page/${i + 1}`)); + * ``` + */ +export const asyncIterablePool = async function* ( + jobs: Iterable>>, + concurrency = 3, +): AsyncIterable { + const pool = new Map>(); + + const first = () => + Promise.race(pool.values()).then(({ sym, val }) => { + pool.delete(sym); + + return val; + }); + + for (const job of jobs) { + const sym = Symbol('job'); + const startedJob = typeof job === 'function' ? job() : job; + pool.set( + sym, + startedJob.then((val) => ({ sym, val })), + ); + + if (pool.size >= concurrency) { + yield await first(); + } + } + + while (pool.size) { + yield await first(); + } +}; + +/** + * Map an (async) iterator with a function. Same as {@see Array.prototype.map()} but for (async) iterators. + * @param it Iterable. + * @param map Mapping function. + * @example + * ```ts + * import { timeout, mapIterator } from '@chialab/sveltekit-utils'; + * + * function* myIt() { + * yield 1; + * yield 2; + * yield 3; + * } + * async function* myAsyncIt() { + * yield 1; + * await timeout(100); + * yield 2; + * yield await timeout(200, 3); + * } + * const addItsIndex = mapIterator( + * myIt(), // Can also be an async iterator: myAsyncIt(), + * (value, idx) => value + index, // Can also be an async function: (value, idx) => timeout(100, value + index), + * ); + * for await (const val of addItsIndex) { + * console.log(val); // 1, 3, 5 + * } + * ``` + */ +export const mapIterator = async function* ( + it: AsyncIterable | Iterable, + map: (value: T, index: number) => K | PromiseLike, +): AsyncGenerator { + let index = 0; + for await (const val of it) { + yield await map(val, index); + index++; + } + + return index; +}; diff --git a/src/lib/url.ts b/src/lib/utils/url.ts similarity index 68% rename from src/lib/url.ts rename to src/lib/utils/url.ts index 22d4362..5a1cb74 100644 --- a/src/lib/url.ts +++ b/src/lib/utils/url.ts @@ -1,3 +1,8 @@ +import { error } from '@sveltejs/kit'; + +/** An URL with `https://` protocol. */ +export type HttpsUrl = `https://${string}`; + /** * Check if URL is "internal" relative to a base URL. * @@ -69,3 +74,34 @@ export const withQueryParams = (base: URL | string, params: EncodableQueryParams return url; }; + +/** + * Get required parameters from a Map-like structure. + * + * @param params Map-like structure where parameters can be `get()` from. + * @param requiredParams List of required parameters. + * @example + * ```ts + * import { getRequiredParams } from '@chialab/sveltekit-utils'; + * + * const { foo, bar } = getRequiredParams(url.searchParams, ['foo', 'bar']); + * ``` + */ +export const getRequiredParams = ( + params: { get(name: T): string | null | undefined }, + requiredParams: readonly T[], +): Record => { + const extracted = requiredParams.reduce>>( + (store, name) => ({ + ...store, + [name]: params.get(name), + }), + {}, + ); + const missingParams = requiredParams.filter((name) => !extracted[name]); + if (missingParams.length) { + error(400, `The following mandatory parameters are missing or empty: ${missingParams.join(', ')}`); + } + + return extracted as Record; +}; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..258824b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,15 @@ +# Tests + +Tests are executed by [Vitest](https://vitest.dev/). + +## Running tests + +Tests can be run with `yarn run test:unit`. Coverage can be generated with `yarn run test:unit:coverage`. + +## File names conventions + +Tests are organised in a structure that recalls as closely as possible the structure of `src/lib/`. + +Tests in files that end in `.browser.test.ts` are only executed in a browser environment (currently headless Chromium). + +Tests in files in the `tests/server/` directory or that end in `.server.test.ts` are only executed in NodeJS. diff --git a/tests/server/sitemap.test.ts b/tests/server/sitemap.test.ts new file mode 100644 index 0000000..e242948 --- /dev/null +++ b/tests/server/sitemap.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it } from 'vitest'; +import { Sitemap, SitemapIndex } from '$lib/server/sitemap'; +import { xml2js } from 'xml-js'; +import { gunzipSync } from 'node:zlib'; + +const toBuf = (body: ReadableStreamReadResult>) => Buffer.from(body.value ?? []); + +describe(Sitemap.name, () => { + const baseUrl = new URL('https://www.example.com/'); + + it('should throw an error when adding duplicate URLs if duplicates are not allowed', () => { + const sitemap = new Sitemap(baseUrl); + sitemap.append({ loc: '/foo' }, false); + + expect(() => sitemap.append({ loc: '/foo' }, false)).to.throw( + 'Location /foo had already been added to this sitemap: duplicate URLs are not allowed', + ); + expect(() => sitemap.append({ loc: '/foo' }, true)).to.not.throw(); + }); + + it('should build an empty sitemap', async () => { + const sitemap = new Sitemap(baseUrl); + + const xml = sitemap.toString(); + const { elements } = xml2js(xml); + expect(elements).deep.equal([ + { + attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' }, + name: 'urlset', + type: 'element', + }, + ]); + + const uncompressed = sitemap.toResponse(false); + expect(uncompressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'), + ); + await expect(uncompressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => toBuf(body)?.toString('utf8') === xml, + ); + + const compressed = sitemap.toResponse(true); + expect(compressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && + response.headers.get('content-encoding') === 'gzip', + ); + await expect(compressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => + toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml, + ); + }); + + it('should build a sitemap with a few URLs', async () => { + const sitemap = new Sitemap(baseUrl) + .append({ loc: '/foo', changeFreq: 'daily' }) + .append({ loc: '/foo', priority: 42 }) + .append({ loc: '/bar' }) + .append({ loc: '/baz', changeFreq: 'monthly', priority: 1, lastMod: new Date('2025-01-01T00:00:00') }); + + const xml = sitemap.toString(); + const { elements } = xml2js(xml); + expect(elements).deep.equal([ + { + attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' }, + name: 'urlset', + type: 'element', + elements: [ + { + name: 'url', + type: 'element', + elements: [ + { name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/foo' }] }, + { name: 'changefreq', type: 'element', elements: [{ type: 'text', text: 'daily' }] }, + ], + }, + { + name: 'url', + type: 'element', + elements: [ + { name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/bar' }] }, + ], + }, + { + name: 'url', + type: 'element', + elements: [ + { name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/baz' }] }, + { name: 'lastmod', type: 'element', elements: [{ type: 'text', text: '2025-01-01' }] }, + { name: 'changefreq', type: 'element', elements: [{ type: 'text', text: 'monthly' }] }, + { name: 'priority', type: 'element', elements: [{ type: 'text', text: '1.00' }] }, + ], + }, + ], + }, + ]); + + const uncompressed = sitemap.toResponse(false); + expect(uncompressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'), + ); + await expect(uncompressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => toBuf(body)?.toString('utf8') === xml, + ); + + const compressed = sitemap.toResponse(true); + expect(compressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && + response.headers.get('content-encoding') === 'gzip', + ); + await expect(compressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => + toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml, + ); + }); +}); + +describe(SitemapIndex.name, () => { + const baseUrl = new URL('https://www.example.com/'); + + it('should build an empty sitemap index', async () => { + const sitemapIndex = new SitemapIndex(baseUrl); + + const xml = sitemapIndex.toString(); + const { elements } = xml2js(xml); + expect(elements).deep.equal([ + { + attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' }, + name: 'sitemapindex', + type: 'element', + }, + ]); + + const uncompressed = sitemapIndex.toResponse(false); + expect(uncompressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'), + ); + await expect(uncompressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => toBuf(body)?.toString('utf8') === xml, + ); + + const compressed = sitemapIndex.toResponse(true); + expect(compressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && + response.headers.get('content-encoding') === 'gzip', + ); + await expect(compressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => + toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml, + ); + }); + + it('should build a sitemap with a few URLs', async () => { + const sitemapIndex = new SitemapIndex(baseUrl) + .append({ loc: '/foo.xml' }) + .append({ loc: '/bar.xml', lastMod: new Date('2025-01-01T00:00:00') }); + + const xml = sitemapIndex.toString(); + const { elements } = xml2js(xml); + expect(elements).deep.equal([ + { + attributes: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' }, + name: 'sitemapindex', + type: 'element', + elements: [ + { + name: 'sitemap', + type: 'element', + elements: [ + { name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/foo.xml' }] }, + ], + }, + { + name: 'sitemap', + type: 'element', + elements: [ + { name: 'loc', type: 'element', elements: [{ type: 'text', text: 'https://www.example.com/bar.xml' }] }, + { name: 'lastmod', type: 'element', elements: [{ type: 'text', text: '2025-01-01' }] }, + ], + }, + ], + }, + ]); + + const uncompressed = sitemapIndex.toResponse(false); + expect(uncompressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && !response.headers.has('content-encoding'), + ); + await expect(uncompressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => toBuf(body)?.toString('utf8') === xml, + ); + + const compressed = sitemapIndex.toResponse(true); + expect(compressed) + .to.be.instanceOf(Response) + .that.satisfies( + (response: Response) => + response.headers.get('content-type') === 'application/xml' && + response.headers.get('content-encoding') === 'gzip', + ); + await expect(compressed.body?.getReader().read()) + .to.be.a('promise') + .that.resolves.satisfies( + (body: ReadableStreamReadResult>) => + toBuf(body).toString('utf8') !== xml && gunzipSync(toBuf(body)).toString('utf8') === xml, + ); + }); +}); diff --git a/tests/server/utils.test.ts b/tests/server/utils.test.ts new file mode 100644 index 0000000..b84dcfe --- /dev/null +++ b/tests/server/utils.test.ts @@ -0,0 +1,91 @@ +import { computeHash, secureId, withTmpDir } from '$lib/server/utils'; +import { existsSync, statSync } from 'node:fs'; +import { basename } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe(secureId.name, () => { + const CASES = { + 'should generate a 32 bytes random hash': { bytes: 32, expectedLength: 64 }, + 'should generate a 18 bytes random hash': { bytes: 18, expectedLength: 36 }, + } satisfies Record; + + Object.entries(CASES).forEach(([label, { bytes, expectedLength }]) => + it(label, () => { + expect(secureId(bytes)).to.be.a('string').with.length(expectedLength); + }), + ); +}); + +describe(withTmpDir.name, () => { + it('should create a temporary directory and remove it upon completion', async () => { + expect.assertions(3); + + const expected = Symbol(); + let tmpDir: string | undefined = undefined; + const result = withTmpDir('foo-', (path) => { + tmpDir = path; + expect(path) + .to.be.a('string') + .that.satisfies((path: string) => basename(path).startsWith('foo-')) + .and.satisfies((path: string) => existsSync(path) && statSync(path).isDirectory()); + + return expected; + }); + + await expect(result).resolves.equal(expected); + + expect(tmpDir).satisfy((path: string) => !existsSync(path)); + }); + + it('should create a temporary directory and remove it after an error is thrown', async () => { + expect.assertions(3); + + const expected = new Error('my error'); + let tmpDir: string | undefined = undefined; + const result = withTmpDir('foo-', (path) => { + tmpDir = path; + expect(path) + .to.be.a('string') + .that.satisfies((path: string) => basename(path).startsWith('foo-')) + .and.satisfies((path: string) => existsSync(path) && statSync(path).isDirectory()); + + throw expected; + }); + + await expect(result).rejects.equal(expected); + + expect(tmpDir).satisfy((path: string) => !existsSync(path)); + }); +}); + +describe(computeHash.name, () => { + const CASES = { + 'should compute md5': { + input: 'password', + expected: '5f4dcc3b5aa765d61d8327deb882cf99', + algorithm: 'md5', + }, + 'should compute sha1': { + input: 'foo bar', + expected: '3773dea65156909838fa6c22825cafe090ff8030', + algorithm: 'sha1', + }, + 'should compute sha256': { + input: 'hello world', + expected: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + algorithm: 'sha256', + }, + 'should compute sha512': { + input: 'example string', + expected: + 'f63ffbf293e2631e013dc2a0958f54f6797f096c36adda6806f717e1d4a314c0fb443ec71eec73cfbd8efa1ad2c709b902066e6356396b97a7ea5191de349012', + algorithm: 'sha512', + }, + } satisfies Record; + + Object.entries(CASES).forEach(([label, { input, algorithm, expected }]) => + it(label, () => { + expect(computeHash(input, algorithm)).equals(expected); + }), + ); +}); diff --git a/tests/test-logger.ts b/tests/test-logger.ts new file mode 100644 index 0000000..f1faafd --- /dev/null +++ b/tests/test-logger.ts @@ -0,0 +1,24 @@ +import type { Level } from 'pino'; + +class DebugTransport { + #data: string[] = []; + + lastLevel?: Level = undefined; + lastMsg?: string = undefined; + lastObj?: unknown = undefined; + lastTime?: number = undefined; + + get [Symbol.for('pino.metadata')]() { + return true; + } + + get writable() { + return true; + } + + write(datum: string): void { + this.#data.push(datum); + } +} + +export const testTransportFactory = () => new DebugTransport(); diff --git a/tests/utils/browser.test.ts b/tests/utils/browser.test.ts new file mode 100644 index 0000000..7be8741 --- /dev/null +++ b/tests/utils/browser.test.ts @@ -0,0 +1,14 @@ +import { sanitizeHtml } from '$lib/utils/browser'; +import { describe, expect, it } from 'vitest'; + +describe(sanitizeHtml.name, async () => { + const CASES = { + 'should remove tag': { html: 'hello world', expected: 'hello world' }, + } satisfies Record; + + Object.entries(CASES).forEach(([label, { html, expected }]) => + it(label, () => { + expect(sanitizeHtml(html)).to.equal(expected); + }), + ); +}); diff --git a/tests/utils/collections.test.ts b/tests/utils/collections.test.ts new file mode 100644 index 0000000..14abcb5 --- /dev/null +++ b/tests/utils/collections.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; +import { entries, group, sift } from '$lib/utils/collections'; + +describe(group.name, () => { + it('should group items when using default comparison and single group', () => { + const items = [ + { foo: 'bar', luckyNumber: 42 }, + { bar: 'baz', luckyNumber: 17 }, + { baz: undefined, luckyNumber: 42 }, + ]; + + expect(group(items, ({ luckyNumber }) => luckyNumber)).to.deep.equal([ + { key: 42, items: [items[0], items[2]] }, + { key: 17, items: [items[1]] }, + ]); + }); + + it('should group items when using default comparison and multiple groups', () => { + const items = [ + { foo: 'bar', luckyNumbers: 42 }, + { bar: 'baz', luckyNumbers: [17] }, + { baz: undefined, luckyNumbers: [42, 17] }, + ]; + + expect(group(items, ({ luckyNumbers }) => luckyNumbers)).to.deep.equal([ + { key: 42, items: [items[0], items[2]] }, + { key: 17, items: [items[1], items[2]] }, + ]); + }); + + it('should group items when using custom comparison and multiple groups', () => { + const items = [ + { foo: 'bar', luckyNumbers: 42 }, + { bar: 'baz', luckyNumbers: [17] }, + { baz: undefined, luckyNumbers: [2, 1] }, + ]; + + expect( + group( + items, + ({ luckyNumbers }) => luckyNumbers, + (a, b) => a % 2 === b % 2, + ), + ).to.deep.equal([ + { key: 42, items: [items[0], items[2]] }, + { key: 17, items: [items[1], items[2]] }, + ]); + }); +}); + +describe(entries.name, () => { + it('should return object entries', () => { + expect(entries({ foo: 'bar', bar: 'baz', baz: -1 })).to.deep.equal([ + ['foo', 'bar'], + ['bar', 'baz'], + ['baz', -1], + ]); + }); +}); + +describe(sift.name, () => { + it('should remove falsy items from array', () => { + const d = new Date(); + expect(sift([null, 1, '', 'foo', undefined, d, false, [], 0])).to.deep.equal([1, 'foo', d, []]); + }); +}); diff --git a/tests/utils/fetch.server.test.ts b/tests/utils/fetch.server.test.ts new file mode 100644 index 0000000..f77a7a8 --- /dev/null +++ b/tests/utils/fetch.server.test.ts @@ -0,0 +1,51 @@ +import { handleFetchError } from '$lib/utils/fetch'; +import { type HttpError, isHttpError } from '@sveltejs/kit'; +import pino from 'pino'; +import { describe, expect, it } from 'vitest'; +import { testTransportFactory } from '../test-logger'; + +describe(handleFetchError.name, () => { + it('should translate a 404 response into a SvelteKit error', () => { + const dest = testTransportFactory(); + const err = new Error('some error', { + cause: new Response(null, { status: 404, headers: { 'Example-Header': 'some; value' } }), + }); + + expect(() => handleFetchError({ foo: 'bar' }, pino(dest))(err)) + .to.throw() + .that.satisfies((err: unknown) => isHttpError(err)) + .and.satisfies((err: HttpError) => err.status === 404); + + expect(dest.lastLevel).to.equal(pino.levels.values['warn']); + expect(dest.lastMsg).to.equal('Resource not found'); + expect(dest.lastObj).to.deep.equal({ status: 404, headers: { 'example-header': 'some; value' }, foo: 'bar' }); + }); + + it('should re-throw an unknown error', () => { + const dest = testTransportFactory(); + const err = { my: 'error' }; + + expect(() => handleFetchError({ foo: 'bar' }, pino(dest))(err)) + .to.throw() + .that.equals(err); + + expect(dest.lastLevel).to.equal(pino.levels.values['error']); + expect(dest.lastMsg).to.equal('Unknown error'); + expect(dest.lastObj).to.deep.equal({ err, foo: 'bar' }); + }); + + it('should re-throw non-404 fetch errors', () => { + const dest = testTransportFactory(); + const err = new Error('some error', { + cause: new Response(null, { status: 400, headers: { 'Example-Header': 'some; value' } }), + }); + + expect(() => handleFetchError({ foo: 'bar' }, pino(dest))(err)) + .to.throw() + .that.equals(err); + + expect(dest.lastLevel).to.equal(pino.levels.values['error']); + expect(dest.lastMsg).to.equal('Unexpected fetch error'); + expect(dest.lastObj).to.deep.equal({ status: 400, headers: { 'example-header': 'some; value' }, foo: 'bar' }); + }); +}); diff --git a/tests/utils/misc.test.ts b/tests/utils/misc.test.ts new file mode 100644 index 0000000..d6298e4 --- /dev/null +++ b/tests/utils/misc.test.ts @@ -0,0 +1,112 @@ +import { backoffRetry, getRandomInt, JitterMode, timeout } from '$lib/utils/misc'; +import { describe, expect, it } from 'vitest'; + +describe(timeout.name, () => { + it('should return void after the requested timeout', async () => { + const start = Date.now(); + const result = await timeout(80); + const end = Date.now(); + + expect(end - start).to.be.approximately(80, 5); + expect(result).to.equal(undefined); + }); + + it('should return the passed result after the timeout', async () => { + const start = Date.now(); + const result = await timeout(120, { foo: 'bar' }); + const end = Date.now(); + + expect(end - start).to.be.approximately(120, 5); + expect(result).to.deep.equal({ foo: 'bar' }); + }); +}); + +describe(getRandomInt.name, () => { + it('should return a random int between 3 and 10', () => { + expect(getRandomInt(3, 10)).to.be.within(3, 10); + }); + + it('should return a random int between 42 and 42', () => { + expect(getRandomInt(42, 42)).to.be.within(42, 42); + }); + + it('should return a random int between 1 and 0', () => { + expect(getRandomInt(1, 0)).to.be.within(0, 1); + }); +}); + +describe(backoffRetry.name, () => { + it('should immediately return the result if the function is successful', async () => { + let count = 0; + const factory = () => { + count++; + + return 'foo'; + }; + + await expect(backoffRetry(factory, 10, 50, 2)).resolves.to.equal('foo'); + expect(count).to.equals(1); + }); + + it('should retry if the function returns undefined', async () => { + let count = 0; + let stopwatch = Date.now(); + const laps: number[] = []; + const factory = () => { + count++; + const now = Date.now(); + laps.push(now - stopwatch); + stopwatch = now; + + return count > 2 ? 'foo' : undefined; + }; + + await expect(backoffRetry(factory, 10, 50, 3, JitterMode.None)).resolves.to.equal('foo'); + expect(count).to.equals(3); + expect(laps).to.have.length(3); + expect(laps[0]).to.be.approximately(0, 5); + expect(laps[1]).to.be.approximately(10, 5); + expect(laps[2]).to.be.approximately(20, 5); + }); + + it('should not wait longer than cap', async () => { + let count = 0; + let stopwatch = Date.now(); + const laps: number[] = []; + const factory = () => { + count++; + const now = Date.now(); + laps.push(now - stopwatch); + stopwatch = now; + + return count > 2 ? 'foo' : undefined; + }; + + await expect(backoffRetry(factory, 10, 15, 3, JitterMode.None)).resolves.to.equal('foo'); + expect(count).to.equals(3); + expect(laps).to.have.length(3); + expect(laps[0]).to.be.approximately(0, 5); + expect(laps[1]).to.be.approximately(10, 5); + expect(laps[2]).to.be.approximately(15, 5); + }); + + it('should not invoke the function more than max attempts times', async () => { + let count = 0; + let stopwatch = Date.now(); + const laps: number[] = []; + const factory = () => { + count++; + const now = Date.now(); + laps.push(now - stopwatch); + stopwatch = now; + + return undefined; + }; + + await expect(backoffRetry(factory, 10, 15, 2, JitterMode.None)).resolves.to.equal(undefined); + expect(count).to.equals(2); + expect(laps).to.have.length(2); + expect(laps[0]).to.be.approximately(0, 5); + expect(laps[1]).to.be.approximately(10, 5); + }); +}); diff --git a/tests/utils/url.test.ts b/tests/utils/url.test.ts new file mode 100644 index 0000000..128bdd2 --- /dev/null +++ b/tests/utils/url.test.ts @@ -0,0 +1,120 @@ +import { + buildURLSearchParams, + getRequiredParams, + isInternalUrl, + withQueryParams, + type EncodableQueryParams, +} from '$lib/utils/url'; +import { isHttpError, type HttpError } from '@sveltejs/kit'; +import { describe, expect, it } from 'vitest'; + +describe(isInternalUrl.name, () => { + it('should detect an internal absolute URL', () => { + expect(isInternalUrl(new URL('https://www.example.org/foo'), 'https://www.example.org/bar')).to.equal(true); + }); + + it('should detect an internal relative URL', () => { + expect(isInternalUrl('/foo', new URL('https://www.example.org/bar'))).to.equal(true); + }); + + it('should detect an external absolute URL', () => { + expect(isInternalUrl('https://www.example.org:3000/bar', new URL('https://www.example.org/bar'))).to.equal(false); + }); + + it('should detect an external relative URL', () => { + expect(isInternalUrl('//www.example.com/foo', 'https://www.example.org/bar')).to.equal(false); + }); +}); + +describe(buildURLSearchParams.name, () => { + const CASES = { + 'should build empty params for empty object': { params: {}, expected: [] }, + 'should build query params for a simple object': { + params: { foo: 'bar', bar: undefined }, + expected: [['foo', 'bar']], + }, + 'should correctly encode arrays': { + params: { foo: ['bar', 'baz'] }, + expected: [ + ['foo[0]', 'bar'], + ['foo[1]', 'baz'], + ], + }, + 'should correctly encode complex structures': { + params: { foo: ['bar', { baz: [1, 2] }, 'barbaz', undefined] }, + expected: [ + ['foo[0]', 'bar'], + ['foo[1][baz][0]', '1'], + ['foo[1][baz][1]', '2'], + ['foo[2]', 'barbaz'], + ], + }, + } satisfies Record; + + Object.entries(CASES).forEach(([label, { params, expected }]) => + it(label, () => { + expect([...buildURLSearchParams(params).entries()]).to.deep.equal(expected); + }), + ); +}); + +describe(withQueryParams.name, () => { + const CASES = { + 'should leave params untouched': { + base: 'https://example.com/?foo=bar', + params: {}, + merge: true, + expected: 'https://example.com/?foo=bar', + }, + 'should clean query params': { + base: 'https://example.com/?foo=bar', + params: {}, + merge: false, + expected: 'https://example.com/', + }, + 'should append and overwrite query params': { + base: 'https://example.com/?foo=bar&bar=baz', + params: { foo: 'barbaz', bar: undefined, baz: '1' }, + merge: true, + expected: 'https://example.com/?foo=barbaz&baz=1', + }, + } satisfies Record; + + Object.entries(CASES).forEach(([label, { base, params, merge = true, expected }]) => + it(label, () => { + expect(withQueryParams(base, params, merge).toString()).to.equal(expected); + }), + ); +}); + +describe(getRequiredParams.name, () => { + it('should throw error if one param is missing', () => { + expect(() => getRequiredParams(new Map([['foo', 'bar']]), ['foo', 'bar'])) + .to.throw() + .that.satisfies((err: unknown) => isHttpError(err)) + .and.satisfies((err: HttpError) => err.status === 400); + }); + + it('should throw error if multiple params are missing', () => { + expect(() => getRequiredParams(new Map([['foo', 'bar']]), ['foo', 'bar', 'baz'])) + .to.throw() + .that.satisfies((err: unknown) => isHttpError(err)) + .and.satisfies((err: HttpError) => err.status === 400); + }); + + it('should return empty object if no parameter are required', () => { + expect(getRequiredParams(new Map([['foo', 'bar']]), [])).to.deep.equals({}); + }); + + it('should return requested parameters if all are present', () => { + expect( + getRequiredParams( + new Map([ + ['foo', 'bar'], + ['bar', 'baz'], + ]), + ['foo', 'bar'], + ), + ).to.deep.equals({ foo: 'bar', bar: 'baz' }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a1b077e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + resolve: { conditions: ['browser'] }, + test: { + coverage: { + include: ['src/lib/**/*.ts'], + reportsDirectory: './tests/coverage', + }, + }, + }), +); diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000..b325ec6 --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,26 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace([ + { + extends: './vitest.config.ts', + test: { + include: ['tests/**/*.{test,spec}.ts', '!tests/**/*.browser.{test,spec}.ts', '!tests/**/browser.{test,spec}.ts'], + name: 'server', + environment: 'node', + }, + }, + { + extends: './vitest.config.ts', + test: { + include: ['tests/**/*.{test,spec}.ts', '!tests/**/*.server.{test,spec}.ts', '!tests/server/**/*.{test,spec}.ts'], + name: 'browser', + browser: { + enabled: true, + provider: 'playwright', + headless: true, + screenshotFailures: false, + instances: [{ browser: 'chromium' }], + }, + }, + }, +]); diff --git a/yarn.lock b/yarn.lock index 520c5bc..b6d703f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,13 +10,74 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/runtime@^7.5.5": +"@babel/code-frame@^7.10.4": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@^7.25.4": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.7.tgz#e114cd099e5f7d17b05368678da0fb9f69b3385c" + integrity sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w== + dependencies: + "@babel/types" "^7.26.7" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5": version "7.26.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341" integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ== dependencies: regenerator-runtime "^0.14.0" +"@babel/types@^7.25.4", "@babel/types@^7.26.7": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.7.tgz#5e2b89c0768e874d4d061961f3a5a153d71dc17a" + integrity sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@bcoe/v8-coverage@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" + integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== + +"@bundled-es-modules/cookie@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507" + integrity sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw== + dependencies: + cookie "^0.7.2" + +"@bundled-es-modules/statuses@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872" + integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg== + dependencies: + statuses "^2.0.1" + +"@bundled-es-modules/tough-cookie@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3" + integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw== + dependencies: + "@types/tough-cookie" "^4.0.5" + tough-cookie "^4.1.4" + "@changesets/apply-release-plan@^7.0.8": version "7.0.8" resolved "https://registry.yarnpkg.com/@changesets/apply-release-plan/-/apply-release-plan-7.0.8.tgz#9cfd46036789ad433fd855f34d41cd9ca1658aa0" @@ -389,6 +450,55 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@inquirer/confirm@^5.0.0": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.5.tgz#0e6bf86794f69f849667ee38815608d6cd5917ba" + integrity sha512-ZB2Cz8KeMINUvoeDi7IrvghaVkYT2RB0Zb31EaLWOE87u276w4wnApv0SH2qWaJ3r0VSUa3BIuz7qAV2ZvsZlg== + dependencies: + "@inquirer/core" "^10.1.6" + "@inquirer/type" "^3.0.4" + +"@inquirer/core@^10.1.6": + version "10.1.6" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.6.tgz#2a92a219cb48c81453e145a5040d0e04f7df1aa2" + integrity sha512-Bwh/Zk6URrHwZnSSzAZAKH7YgGYi0xICIBDFOqBQoXNNAzBHw/bgXgLmChfp+GyR3PnChcTbiCTZGC6YJNJkMA== + dependencies: + "@inquirer/figures" "^1.0.10" + "@inquirer/type" "^3.0.4" + ansi-escapes "^4.3.2" + cli-width "^4.1.0" + mute-stream "^2.0.0" + signal-exit "^4.1.0" + wrap-ansi "^6.2.0" + yoctocolors-cjs "^2.1.2" + +"@inquirer/figures@^1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.10.tgz#e3676a51c9c51aaabcd6ba18a28e82b98417db37" + integrity sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw== + +"@inquirer/type@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.4.tgz#fa5f9e91a0abf3c9e93d3e1990ecb891d8195cf2" + integrity sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + "@jridgewell/gen-mapping@^0.3.5": version "0.3.8" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" @@ -413,7 +523,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -443,6 +553,18 @@ globby "^11.0.0" read-yaml-file "^1.1.0" +"@mswjs/interceptors@^0.37.0": + version "0.37.6" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.37.6.tgz#2635319b7a81934e1ef1b5593ef7910347e2b761" + integrity sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w== + dependencies: + "@open-draft/deferred-promise" "^2.2.0" + "@open-draft/logger" "^0.3.0" + "@open-draft/until" "^2.0.0" + is-node-process "^1.2.0" + outvariant "^1.4.3" + strict-event-emitter "^0.5.1" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -464,6 +586,29 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@open-draft/deferred-promise@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== + +"@open-draft/logger@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== + dependencies: + is-node-process "^1.2.0" + outvariant "^1.4.0" + +"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@polka/url@^1.0.0-next.24": version "1.0.0-next.28" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" @@ -650,12 +795,36 @@ magic-string "^0.30.15" vitefu "^1.0.4" +"@testing-library/dom@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" + integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/user-event@^14.6.1": + version "14.6.1" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" + integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== + +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/cookie@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== -"@types/estree@1.0.6", "@types/estree@^1.0.5", "@types/estree@^1.0.6": +"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== @@ -672,6 +841,16 @@ dependencies: undici-types "~6.20.0" +"@types/statuses@^2.0.4": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63" + integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A== + +"@types/tough-cookie@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@typescript-eslint/eslint-plugin@^8.15.0": version "8.22.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz#63a1b0d24d85a971949f8d71d693019f58d2e861" @@ -758,6 +937,98 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== +"@vitest/browser@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-3.0.5.tgz#d0497e44592fdd94bb477ab7adcf00989c9225ba" + integrity sha512-5WAWJoucuWcGYU5t0HPBY03k9uogbUEIu4pDmZHoB4Dt+6pXqzDbzEmxGjejZSitSYA3k/udYfuotKNxETVA3A== + dependencies: + "@testing-library/dom" "^10.4.0" + "@testing-library/user-event" "^14.6.1" + "@vitest/mocker" "3.0.5" + "@vitest/utils" "3.0.5" + magic-string "^0.30.17" + msw "^2.7.0" + sirv "^3.0.0" + tinyrainbow "^2.0.0" + ws "^8.18.0" + +"@vitest/coverage-v8@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz#22a5f6730f13703ce6736f7d0032251a3619a300" + integrity sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg== + dependencies: + "@ampproject/remapping" "^2.3.0" + "@bcoe/v8-coverage" "^1.0.2" + debug "^4.4.0" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.6" + istanbul-reports "^3.1.7" + magic-string "^0.30.17" + magicast "^0.3.5" + std-env "^3.8.0" + test-exclude "^7.0.1" + tinyrainbow "^2.0.0" + +"@vitest/expect@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.0.5.tgz#aa0acd0976cf56842806e5dcaebd446543966b14" + integrity sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA== + dependencies: + "@vitest/spy" "3.0.5" + "@vitest/utils" "3.0.5" + chai "^5.1.2" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.0.5.tgz#8dce3dc4cb0adfd9d554531cea836244f8c36bcd" + integrity sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw== + dependencies: + "@vitest/spy" "3.0.5" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.0.5", "@vitest/pretty-format@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.0.5.tgz#10ae6a83ccc1a866e31b2d0c1a7a977ade02eff9" + integrity sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.0.5.tgz#c5960a1169465a2b9ac21f1d24a4cf1fe67c7501" + integrity sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A== + dependencies: + "@vitest/utils" "3.0.5" + pathe "^2.0.2" + +"@vitest/snapshot@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.0.5.tgz#afd0ae472dc5893b0bb10e3e673ef649958663f4" + integrity sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA== + dependencies: + "@vitest/pretty-format" "3.0.5" + magic-string "^0.30.17" + pathe "^2.0.2" + +"@vitest/spy@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.0.5.tgz#7bb5d84ec21cc0d62170fda4e31cd0b46c1aeb8b" + integrity sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.0.5.tgz#dc3eaefd3534598917e939af59d9a9b6a5be5082" + integrity sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg== + dependencies: + "@vitest/pretty-format" "3.0.5" + loupe "^3.1.2" + tinyrainbow "^2.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -788,18 +1059,40 @@ ansi-colors@^4.1.1, ansi-colors@^4.1.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.1.0: +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -812,6 +1105,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" @@ -822,6 +1122,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -866,12 +1171,28 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chalk@^4.0.0: +chai@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" + integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -884,6 +1205,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + chokidar@^4.0.0, chokidar@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" @@ -896,6 +1222,20 @@ ci-info@^3.7.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" @@ -933,12 +1273,17 @@ cookie@^0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cookie@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== -cross-spawn@^7.0.2, cross-spawn@^7.0.5: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -957,7 +1302,7 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0: +debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -969,6 +1314,11 @@ dedent-js@^1.0.1: resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305" integrity sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ== +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -979,6 +1329,11 @@ deepmerge@^4.3.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" @@ -1003,6 +1358,26 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1018,6 +1393,11 @@ enquirer@^2.4.1: ansi-colors "^4.1.1" strip-ansi "^6.0.1" +es-module-lexer@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + esbuild@^0.24.2: version "0.24.2" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" @@ -1049,6 +1429,11 @@ esbuild@^0.24.2: "@esbuild/win32-ia32" "0.24.2" "@esbuild/win32-x64" "0.24.2" +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -1190,11 +1575,23 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2, esutils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + extendable-error@^0.1.5: version "0.1.7" resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.7.tgz#60b9adf206264ac920058a7395685ae4670c2b96" @@ -1306,6 +1703,14 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -1329,6 +1734,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -1339,6 +1749,11 @@ generic-pool@3.9.0: resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1353,6 +1768,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^10.4.1: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1394,16 +1821,31 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql@^16.8.1: + version "16.10.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" + integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +headers-polyfill@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" + integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== + help-me@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + human-id@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-id/-/human-id-1.0.2.tgz#e654d4b2b0d8b07e45da9f6020d8af17ec0a5df3" @@ -1457,6 +1899,11 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -1464,6 +1911,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-node-process@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -1498,11 +1950,56 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + +istanbul-reports@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + joycon@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^3.13.1, js-yaml@^3.6.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -1599,6 +2096,11 @@ lodash.startcase@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== +loupe@^3.1.0, loupe@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" + integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -1606,13 +2108,39 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -magic-string@^0.30.11, magic-string@^0.30.15, magic-string@^0.30.5: +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +magic-string@^0.30.11, magic-string@^0.30.15, magic-string@^0.30.17, magic-string@^0.30.5: version "0.30.17" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" +magicast@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739" + integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== + dependencies: + "@babel/parser" "^7.25.4" + "@babel/types" "^7.25.4" + source-map-js "^1.2.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1645,6 +2173,11 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mri@^1.1.0, mri@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -1660,6 +2193,35 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.0.tgz#d13ff87f7e018fc4c359800ff72ba5017033fb56" + integrity sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw== + dependencies: + "@bundled-es-modules/cookie" "^2.0.1" + "@bundled-es-modules/statuses" "^1.0.1" + "@bundled-es-modules/tough-cookie" "^0.1.6" + "@inquirer/confirm" "^5.0.0" + "@mswjs/interceptors" "^0.37.0" + "@open-draft/deferred-promise" "^2.2.0" + "@open-draft/until" "^2.1.0" + "@types/cookie" "^0.6.0" + "@types/statuses" "^2.0.4" + graphql "^16.8.1" + headers-polyfill "^4.0.2" + is-node-process "^1.2.0" + outvariant "^1.4.3" + path-to-regexp "^6.3.0" + picocolors "^1.1.1" + strict-event-emitter "^0.5.1" + type-fest "^4.26.1" + yargs "^17.7.2" + +mute-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== + nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" @@ -1712,6 +2274,11 @@ outdent@^0.5.0: resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== +outvariant@^1.4.0, outvariant@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== + p-filter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" @@ -1757,6 +2324,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + package-manager-detector@^0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-0.2.9.tgz#20990785afa69d38b4520ccc83b34e9f69cb970f" @@ -1792,11 +2364,34 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.2.tgz#5ed86644376915b3c7ee4d00ac8c348d671da3a5" + integrity sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w== + +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -1860,6 +2455,20 @@ pino@^9.5.0: sonic-boom "^4.0.1" thread-stream "^3.0.0" +playwright-core@1.50.1: + version "1.50.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.50.1.tgz#6a0484f1f1c939168f40f0ab3828c4a1592c4504" + integrity sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ== + +playwright@^1.50.1: + version "1.50.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.50.1.tgz#2f93216511d65404f676395bfb97b41aa052b180" + integrity sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw== + dependencies: + playwright-core "1.50.1" + optionalDependencies: + fsevents "2.3.2" + postcss-load-config@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" @@ -1915,11 +2524,27 @@ prettier@^3.3.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + process-warning@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb" integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q== +psl@^1.1.33: + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + pump@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" @@ -1928,11 +2553,16 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -1943,6 +2573,11 @@ quick-format-unescaped@^4.0.3: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + read-yaml-file@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-1.1.0.tgz#9362bbcbdc77007cc8ea4519fe1c0b821a7ce0d8" @@ -1980,6 +2615,16 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2086,7 +2731,12 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^4.0.1: +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -2112,7 +2762,7 @@ sonic-boom@^4.0.1: dependencies: atomic-sleep "^1.0.0" -source-map-js@^1.2.1: +source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -2135,13 +2785,74 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -strip-ansi@^6.0.1: +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +statuses@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +std-env@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + +strict-event-emitter@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -2214,6 +2925,15 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== +test-exclude@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2" + integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^9.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2226,6 +2946,31 @@ thread-stream@^3.0.0: dependencies: real-require "^0.2.0" +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -2245,6 +2990,16 @@ totalist@^3.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== +tough-cookie@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + ts-api-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900" @@ -2267,6 +3022,16 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^4.26.1: + version "4.33.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.33.0.tgz#2da0c135b9afa76cf8b18ecfd4f260ecd414a432" + integrity sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g== + typescript@^5.6.3: version "5.7.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" @@ -2282,6 +3047,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2289,12 +3059,31 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vite@^6.0.0: +vite-node@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.0.5.tgz#6a0d06f7a4bdaae6ddcdedc12d910d886cf7d62f" + integrity sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A== + dependencies: + cac "^6.7.14" + debug "^4.4.0" + es-module-lexer "^1.6.0" + pathe "^2.0.2" + vite "^5.0.0 || ^6.0.0" + +"vite@^5.0.0 || ^6.0.0", vite@^6.0.0: version "6.0.11" resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.11.tgz#224497e93e940b34c3357c9ebf2ec20803091ed8" integrity sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg== @@ -2310,6 +3099,32 @@ vitefu@^1.0.4: resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.0.5.tgz#eab501e07da167bbb68e957685823e6b425e7ce2" integrity sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA== +vitest@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.0.5.tgz#a9a3fa1203d85869c9ba66f3ea990b72d00ddeb0" + integrity sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q== + dependencies: + "@vitest/expect" "3.0.5" + "@vitest/mocker" "3.0.5" + "@vitest/pretty-format" "^3.0.5" + "@vitest/runner" "3.0.5" + "@vitest/snapshot" "3.0.5" + "@vitest/spy" "3.0.5" + "@vitest/utils" "3.0.5" + chai "^5.1.2" + debug "^4.4.0" + expect-type "^1.1.0" + magic-string "^0.30.17" + pathe "^2.0.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinypool "^1.0.2" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0" + vite-node "3.0.5" + why-is-node-running "^2.3.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -2317,16 +3132,65 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml-js@^1.6.11: version "1.6.11" resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" @@ -2334,6 +3198,11 @@ xml-js@^1.6.11: dependencies: sax "^1.2.4" +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -2344,11 +3213,34 @@ yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoctocolors-cjs@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" + integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== + zimmerframe@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/zimmerframe/-/zimmerframe-1.1.2.tgz#5b75f1fa83b07ae2a428d51e50f58e2ae6855e5e"