sveltekit: Setup unit tests with vitest (#54953)

This PR adds vitest and faker for unit testing, and to use it properly
already I refactored the promise->store helper to be more flexible.

**Unit testing**
vitest works prefectly together with vite (it's from the same
author/community). It will use the same configuration and so there is
very little additional configuration necessary.
I only had to update vite.config.ts to not overwrite `process` (but
according to https://vitejs.dev/config/shared-options.html#define I
might not be doing it right anyway... will look into this another time).

The API is pretty compatible with jest, so there shouldn't be any
surprises.

Tests can be run with `pnpm vitest`.

**Faker**
I stared to use faker on a differnt branch to generate more (and more
realistic) test data for storybook stories and unit test. Eventually I'd
like to use this to generate mock data for any of our GraphQL APIs. One
great feature is the ability to _seed_ the random number generator so
that you can get random but repeatable values in tests.

**Promise<>store utility**
Working with promises in a reactive way can be tricky. There is a risk
of stale data ovewriting current data when an older promise resolves
after a newer one.
Observables can help here but since we are trying to move away from
them, I introduced a simple store to handle promises. I extended it now
to handle more cases, especially being able to access the previous value
while a new promise is loading. The API might seem clunky (and I'd be
happy to improve it eventually), but this way makes it easier to
remember to call `set` whenever the promise changes.



## Test plan

`pnpm vitest`

Run dev server, open pages affected by promise store changes (repo
pages) and verify that they behave as expected.
This commit is contained in:
Felix Kling 2023-07-19 16:58:29 +02:00 committed by GitHub
parent 406c2afea8
commit fff87e4a50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 817 additions and 249 deletions

View File

@ -14,6 +14,8 @@
# gazelle:js_test_files **/__mocks__/**/*.{ts,tsx}
# gazelle:js_test_files **/fixtures/**/*.{ts,tsx}
# gazelle:js_test_files **/WebStory.{ts,tsx}
# TODO(bazel): sveltekit tests
# gazelle:exclude **/web-sveltekit/**/*.test.ts
# TODO(bazel): put fixtures + testutils + ? into own rules
# js_{fixture}_files **/*.{fixture,fixtures}.{ts,tsx}

View File

@ -18,6 +18,7 @@
"build-storybook": "storybook build"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@playwright/test": "1.25.0",
"@storybook/addon-essentials": "^7.0.26",
"@storybook/addon-interactions": "^7.0.26",
@ -41,7 +42,8 @@
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"tslib": "2.1.0",
"vite": "^4.3.9"
"vite": "^4.3.9",
"vitest": "^0.33.0"
},
"type": "module",
"dependencies": {

View File

@ -3,9 +3,9 @@
import type { GitCommitFields } from '$lib/graphql-operations'
import Icon from '$lib/Icon.svelte'
import { getRelativeTime } from '$lib/relativeTime'
import { currentDate as now } from '$lib/stores'
import UserAvatar from '$lib/UserAvatar.svelte'
import { getRelativeTime } from '$lib/utils'
export let commit: GitCommitFields
export let alwaysExpanded: boolean = false

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { numberWithCommas } from '$lib/common'
import type { GitRefFields } from '$lib/graphql-operations'
import { getRelativeTime } from '$lib/relativeTime'
import { currentDate as now } from '$lib/stores'
import { getRelativeTime } from '$lib/utils'
export let ref: GitRefFields

View File

@ -93,7 +93,7 @@ export const fetchTreeEntries = memoizeObservable(
const MAX_FILE_TREE_ENTRIES = 1000
export const NODE_LIMIT: unique symbol = Symbol()
type ExpandableFileTreeNodeValues = TreeEntryFields
type FileTreeNodeValue = ExpandableFileTreeNodeValues | typeof NODE_LIMIT
export type FileTreeNodeValue = ExpandableFileTreeNodeValues | typeof NODE_LIMIT
export const fetchSidebarFileTree = memoize(
async ({

View File

@ -4,7 +4,6 @@ import { logger } from '$lib/common'
import { type TemporarySettings, TemporarySettingsStorage, migrateLocalStorageToTemporarySettings } from '$lib/shared'
import { getStores } from './stores'
import type { LoadingData } from './utils'
const loggedOutUserStore = new TemporarySettingsStorage(null, false)
@ -27,6 +26,11 @@ export function createTemporarySettingsStorage(storage = loggedOutUserStore): Wr
}
}
type LoadingData<D, E> =
| { loading: true }
| { loading: false; data: D; error: null }
| { loading: false; data: null; error: E }
type TemporarySettingsKey = keyof TemporarySettings
type TemporarySettingStatus<K extends TemporarySettingsKey> = LoadingData<TemporarySettings[K], unknown>

View File

@ -1,90 +0,0 @@
import type { Observable } from 'rxjs'
import { shareReplay } from 'rxjs/operators'
import { type Readable, type Writable, writable, get, type Unsubscriber } from 'svelte/store'
export type LoadingData<D, E = Error> =
| { loading: true }
| { loading: false; data: D; error: null }
| { loading: false; data: null; error: E }
/**
* Converts a promise to a readable store which emits data, loading and error states.
* Sometimes load functions return deferred promises and the data needs to be
* "post processed" in code (i.e. not using {#await}).
* Usually when working with async data one has to be careful with outdated data.
* If the load function has been called again we don't want to process the
* previous data anymore.
* Using a (reactive) store makes that simpler since Svelte will automatically unsubscribe
* when the store changes.
*/
export function asStore<T, E = Error>(
promise: Promise<T>
): Readable<LoadingData<T, E>> & { set(promise: Promise<T>): void } {
const { subscribe, set } = writable<LoadingData<T, E>>({ loading: true })
function process(currentPromise: Promise<T>) {
promise = currentPromise
currentPromise.then(
result => {
if (currentPromise === promise) {
set({ loading: false, data: result, error: null })
}
},
error => {
if (currentPromise === promise) {
set({ loading: false, data: null, error })
}
}
)
}
process(promise)
return {
subscribe,
set: process,
}
}
/**
* Helper function to convert an Observable to a Svelte Readable. Useful when a
* real Readable is needed to satisfy an interface.
*/
export function readableObservable<T>(observable: Observable<T>): Readable<T> {
const sharedObservable = observable.pipe(shareReplay(1))
return {
subscribe(subscriber) {
const subscription = sharedObservable.subscribe(subscriber)
return () => subscription.unsubscribe()
},
}
}
/**
* Returns a helper store that syncs with the currently set store.
*/
export function createForwardStore<T>(store: Writable<T>): Writable<T> & { updateStore(store: Writable<T>): void } {
const { subscribe, set } = writable<T>(get(store), () => link(store))
let unsubscribe: Unsubscriber | null = null
function link(store: Writable<T>): Unsubscriber {
unsubscribe?.()
return (unsubscribe = store.subscribe(set))
}
return {
subscribe,
set(value) {
store.set(value)
},
update(value) {
store.update(value)
},
updateStore(newStore) {
if (newStore !== store) {
store = newStore
link(store)
}
},
}
}

View File

@ -0,0 +1,45 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`getRelativeTime > random times 1`] = `"2 years ago"`;
exports[`getRelativeTime > random times 2`] = `"2 years ago"`;
exports[`getRelativeTime > random times 3`] = `"last year"`;
exports[`getRelativeTime > random times 4`] = `"last year"`;
exports[`getRelativeTime > random times 5`] = `"10 months ago"`;
exports[`getRelativeTime > random times 6`] = `"10 months ago"`;
exports[`getRelativeTime > random times 7`] = `"6 months ago"`;
exports[`getRelativeTime > random times 8`] = `"5 months ago"`;
exports[`getRelativeTime > random times 9`] = `"5 months ago"`;
exports[`getRelativeTime > random times 10`] = `"last month"`;
exports[`getRelativeTime > specific times > days 1`] = `"12 days ago"`;
exports[`getRelativeTime > specific times > hours 1`] = `"7 hours ago"`;
exports[`getRelativeTime > specific times > last day 1`] = `"24 hours ago"`;
exports[`getRelativeTime > specific times > last hour 1`] = `"60 minutes ago"`;
exports[`getRelativeTime > specific times > last minute 1`] = `"60 seconds ago"`;
exports[`getRelativeTime > specific times > last month 1`] = `"last month"`;
exports[`getRelativeTime > specific times > last second 1`] = `"1 second ago"`;
exports[`getRelativeTime > specific times > last year 1`] = `"12 months ago"`;
exports[`getRelativeTime > specific times > minutes 1`] = `"25 minutes ago"`;
exports[`getRelativeTime > specific times > months 1`] = `"4 months ago"`;
exports[`getRelativeTime > specific times > seconds 1`] = `"25 seconds ago"`;
exports[`getRelativeTime > specific times > years 1`] = `"8 years ago"`;

View File

@ -0,0 +1,3 @@
export * from './promises'
export * from './time'
export * from './stores'

View File

@ -0,0 +1,107 @@
import { get } from 'svelte/store'
import { describe, it, vi, beforeAll, afterAll, expect } from 'vitest'
import { createPromiseStore } from './promises'
beforeAll(() => {
vi.useFakeTimers()
})
afterAll(() => {
vi.useRealTimers()
})
describe('createPromiseStore', () => {
describe('initial promise', () => {
it('correctly updates each store for resolved initial promises', async () => {
const { pending, value, error, set } = createPromiseStore<number>()
set(Promise.resolve(1))
expect(get(pending)).toBe(true)
expect(get(value)).toBe(null)
expect(get(error)).toBe(null)
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
expect(get(value)).toBe(1)
expect(get(error)).toBe(null)
})
it('correctly updates each store for rejected initial promises', async () => {
const { pending, value, error, set } = createPromiseStore<number>()
set(Promise.reject(1))
expect(get(pending)).toBe(true)
expect(get(value)).toBe(null)
expect(get(error)).toBe(null)
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
expect(get(value)).toBe(null)
expect(get(error)).toBe(1)
})
})
describe('updates', () => {
it('updates the store values when a new promise is set', async () => {
const { pending, value, error, set } = createPromiseStore<number>()
set(Promise.resolve(1))
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
set(Promise.reject(2))
expect(get(pending)).toBe(true)
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
expect(get(value)).toBe(null)
expect(get(error)).toBe(2)
set(Promise.resolve(3))
expect(get(pending)).toBe(true)
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
expect(get(value)).toBe(3)
expect(get(error)).toBe(null)
})
it('updates the store with the latest resolved promise', async () => {
const { pending, value, set } = createPromiseStore<number>()
set(Promise.resolve(1))
set(Promise.resolve(2))
await vi.runOnlyPendingTimersAsync()
expect(get(pending)).toBe(false)
expect(get(value)).toBe(2)
})
it('retains the old value while a new promise is resolved', async () => {
const { pending, value, latestValue, set } = createPromiseStore<number>()
set(Promise.resolve(1))
await vi.runOnlyPendingTimersAsync()
set(Promise.resolve(2))
expect(get(pending)).toBe(true)
expect(get(value)).toBe(null)
expect(get(latestValue)).toBe(1)
})
it('retains the old error while a new promise is resolved', async () => {
const { pending, error, latestError, set } = createPromiseStore<number>()
set(Promise.reject(1))
await vi.runOnlyPendingTimersAsync()
set(Promise.resolve(2))
expect(get(pending)).toBe(true)
expect(get(error)).toBe(null)
expect(get(latestError)).toBe(1)
})
})
})

View File

@ -0,0 +1,92 @@
import { type Readable, writable, readonly, derived } from 'svelte/store'
interface PromiseStore<D, E = Error> {
/**
* True when the promise is pending, false otherwise.
* Initial value: false
*/
pending: Readable<boolean>
/**
* The current value or null if the current promise was rejected or is pending.
* Initial value: null
*/
value: Readable<D | null>
/**
* The current error or null if the current promise was resolved or is pending.
* Initial value: null
*/
error: Readable<E | null>
/**
* The value of the latest settled promise. While a new promise is pending this will contain
* the value of the previously settled promise (or null if the promise was rejected).
* Initial value: null
*/
latestValue: Readable<D | null>
/**
* The value of the latest promise. While a new promise is pending this will contain
* the value of the previously settled promise (or null if the promise was resolved).
* Initial value: null
*/
latestError: Readable<E | null>
/**
* Sets the passed promise as the current promise and tracks its status.
* Does nothing if the same promise as the current one is passed. The argument
* is optional to make it easier to work with optional data coming from loaders.
*/
set: (promise?: Promise<D> | null) => void
}
/**
* Returns multiple stores to track the promises state, resolved value and rejection error.
* The store ensures that `value` is updated with latest resolved promise.
*/
export function createPromiseStore<D, E = Error>(): PromiseStore<Awaited<D>, E> {
let currentPromise: Promise<Awaited<D>> | null | undefined
const pending = writable<boolean>(false)
const value = writable<Awaited<D> | null>(null)
const error = writable<E | null>(null)
function resolve(promise?: Promise<Awaited<D>> | null) {
currentPromise = promise
if (!promise) {
value.set(null)
error.set(null)
pending.set(false)
return
}
pending.set(true)
promise.then(
result => {
if (currentPromise === promise) {
value.set(result)
error.set(null)
pending.set(false)
}
},
errorValue => {
if (currentPromise === promise) {
value.set(null)
error.set(errorValue)
pending.set(false)
}
}
)
}
resolve(currentPromise)
return {
pending: readonly(pending),
value: derived([pending, value], ([$pending, $value]) => ($pending ? null : $value)),
error: derived([pending, error], ([$pending, $error]) => ($pending ? null : $error)),
latestValue: readonly(value),
latestError: readonly(error),
set: promise => {
if (promise !== currentPromise) {
resolve(promise)
}
},
}
}

View File

@ -0,0 +1,41 @@
import { get, writable } from 'svelte/store'
import { describe, it, expect } from 'vitest'
import { createForwardStore } from './stores'
describe('createForwardStore', () => {
it('syncs the initial value of the passed store', () => {
const store = createForwardStore(writable(1))
expect(get(store)).toBe(1)
})
it('updates when the passed store updates', () => {
const origin = writable(1)
const store = createForwardStore(origin)
origin.set(2)
expect(get(store)).toBe(2)
})
it('syncs with a new store', () => {
const origin = writable(1)
const store = createForwardStore(origin)
store.updateStore(writable(2))
// Update the original store to verify that the forward store doesn't
// subscribe to it anymore
origin.set(3)
expect(get(store)).toBe(2)
})
it('updates the original store', () => {
const origin = writable(1)
const store = createForwardStore(origin)
store.set(2)
expect(get(origin)).toBe(2)
})
})

View File

@ -0,0 +1,30 @@
import { get, writable, type Unsubscriber, type Writable } from 'svelte/store'
/**
* Returns a helper store that syncs with the currently set store.
*/
export function createForwardStore<T>(store: Writable<T>): Writable<T> & { updateStore(store: Writable<T>): void } {
const { subscribe, set } = writable<T>(get(store), () => link(store))
let unsubscribe: Unsubscriber | null = null
function link(store: Writable<T>): Unsubscriber {
unsubscribe?.()
return (unsubscribe = store.subscribe(set))
}
return {
subscribe,
set(value) {
store.set(value)
},
update(value) {
store.update(value)
},
updateStore(newStore) {
if (newStore !== store) {
store = newStore
link(store)
}
},
}
}

View File

@ -0,0 +1,68 @@
import { faker } from '@faker-js/faker'
import { it, vi, beforeAll, afterAll, expect, describe } from 'vitest'
import { getRelativeTime } from './time'
const defaults = {
Y: 2023,
M: 5,
D: 14,
h: 12,
m: 30,
s: 30,
}
function d(options?: Partial<typeof defaults>): Date {
const combined = { ...defaults, ...options }
return new Date(combined.Y, combined.M, combined.D, combined.h, combined.m, combined.s)
}
beforeAll(() => {
vi.useFakeTimers()
vi.setSystemTime(d())
})
afterAll(() => {
vi.useRealTimers()
})
describe('getRelativeTime', () => {
it('uses the current time as reference by default', () => {
expect(getRelativeTime(d({ h: 3 }))).toMatchInlineSnapshot('"9 hours ago"')
})
it('uses the provided reference date', () => {
expect(getRelativeTime(d({ D: 5 }), d({ D: 10 }))).toMatchInlineSnapshot('"5 days ago"')
})
describe('specific times', () => {
it.each([
['last second', d({ s: 29 })],
['seconds', d({ s: 5 })],
['last minute', d({ m: 29 })],
['minutes', d({ m: 5 })],
['last hour', d({ h: 11 })],
['hours', d({ h: 5 })],
['last day', d({ D: 13 })],
['days', d({ D: 2 })],
['last month', d({ M: 4 })],
['months', d({ M: 1 })],
['last year', d({ Y: 2022 })],
['years', d({ Y: 2015 })],
])('%s', (_, date) => {
expect(getRelativeTime(date)).toMatchSnapshot()
})
})
it('random times', () => {
faker.seed(42)
faker.setDefaultRefDate(d())
for (const date of faker.date.betweens({ from: d({ Y: 2021 }), to: d(), count: 10 })) {
expect(getRelativeTime(date)).toMatchSnapshot()
}
faker.seed()
faker.setDefaultRefDate()
})
})

View File

@ -53,7 +53,7 @@
$: ({ repoName, revision, parentPath, resolvedRevision } = data)
$: commitID = isErrorLike(resolvedRevision) ? '' : resolvedRevision.commitID
// Only update the file tree provider (which causes the tree to rerender) when repo, revision/commit or file path
// changes
// update
$: updateFileTreeProvider(repoName, revision, commitID, parentPath)
const sidebarSize = getSeparatorPosition('repo-sidebar', 0.2)

View File

@ -8,14 +8,20 @@
import { NODE_LIMIT } from '$lib/repo/api/tree'
import SidebarToggleButton from '$lib/repo/SidebarToggleButton.svelte'
import { sidebarOpen } from '$lib/repo/stores'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: treeEntries = asStore(data.fileTree.deferred.then(({ values }) => values))
$: commits = asStore(data.commits.deferred)
const { value: treeOrError, set: setTree } = createPromiseStore<typeof data.fileTree.deferred>()
const {
pending: loadingCommits,
value: commits,
set: setCommits,
} = createPromiseStore<typeof data.commits.deferred>()
$: setTree(data.fileTree.deferred)
$: setCommits(data.commits.deferred)
</script>
{#if !$sidebarOpen}
@ -32,10 +38,10 @@
</p>
{/if}
{#if !$treeEntries.loading && $treeEntries.data}
{#if $treeOrError && !isErrorLike($treeOrError)}
<h3>Files and directories</h3>
<ul class="files">
{#each $treeEntries.data as entry}
{#each $treeOrError.values as entry}
<li>
{#if entry !== NODE_LIMIT}
<a
@ -53,10 +59,10 @@
<h3 class="mt-3">Changes</h3>
<ul class="commits">
{#if $commits.loading}
{#if $loadingCommits}
<LoadingSpinner />
{:else if $commits.data}
{#each $commits.data as commit (commit.url)}
{:else if $commits}
{#each $commits as commit (commit.url)}
<li><Commit {commit} /></li>
{/each}
{/if}

View File

@ -3,10 +3,9 @@
import { page } from '$app/stores'
import CodeMirrorBlob from '$lib/CodeMirrorBlob.svelte'
import type { BlobFileFields } from '$lib/graphql-operations'
import Icon from '$lib/Icon.svelte'
import FileHeader from '$lib/repo/FileHeader.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
import FormatAction from './FormatAction.svelte'
@ -14,14 +13,13 @@
export let data: PageData
$: blob = asStore(data.blob.deferred)
$: highlights = asStore(data.highlights.deferred)
$: loading = $blob.loading
let blobData: BlobFileFields
$: if (!$blob.loading && $blob.data) {
blobData = $blob.data
}
$: formatted = !!blobData?.richHTML
// We use the latest value here because we want to keep showing the old document while loading
// the new one.
const { pending: loading, latestValue: blobData, set: setBlob } = createPromiseStore<typeof data.blob.deferred>()
const { value: highlights, set: setHighlights } = createPromiseStore<typeof data.highlights.deferred>()
$: setBlob(data.blob.deferred)
$: setHighlights(data.highlights.deferred)
$: formatted = !!$blobData?.richHTML
$: showRaw = $page.url.searchParams.get('view') === 'raw'
</script>
@ -37,18 +35,15 @@
</svelte:fragment>
</FileHeader>
<div class="content" class:loading>
{#if blobData}
{#if blobData.richHTML && !showRaw}
<div class="content" class:loading={$loading}>
{#if $blobData}
{#if $blobData.richHTML && !showRaw}
<div class="rich">
{@html blobData.richHTML}
{@html $blobData.richHTML}
</div>
{:else}
<CodeMirrorBlob
blob={blobData}
highlights={($highlights && !$highlights.loading && $highlights.data) || ''}
wrapLines={$lineWrap}
/>
<!-- TODO: ensure that only the highlights for the currently loaded blob data are used -->
<CodeMirrorBlob blob={$blobData} highlights={$highlights || ''} wrapLines={$lineWrap} />
{/if}
{/if}
</div>

View File

@ -4,15 +4,15 @@
import { isErrorLike } from '$lib/common'
import Icon from '$lib/Icon.svelte'
import FileHeader from '$lib/repo/FileHeader.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: treeDataStatus = asStore(data.treeEntries.deferred)
$: treeOrError = (!$treeDataStatus.loading && $treeDataStatus.data) || null
$: entries = treeOrError && !isErrorLike(treeOrError) ? treeOrError.entries : []
const { value: treeOrError, set } = createPromiseStore<typeof data.treeEntries.deferred>()
$: set(data.treeEntries.deferred)
$: entries = $treeOrError && !isErrorLike($treeOrError) ? $treeOrError.entries : []
</script>
<FileHeader>
@ -22,16 +22,14 @@
<div class="content">
<h2>Files and directories</h2>
<ul>
{#if treeOrError}
{#each entries as entry}
<li>
<a href={entry.url}>
<Icon svgPath={entry.isDirectory ? mdiFolderOutline : mdiFileDocumentOutline} inline />
{entry.name}
</a>
</li>
{/each}
{/if}
{#each entries as entry}
<li>
<a href={entry.url}>
<Icon svgPath={entry.isDirectory ? mdiFolderOutline : mdiFileDocumentOutline} inline />
{entry.name}
</a>
</li>
{/each}
</ul>
</div>

View File

@ -2,13 +2,13 @@
<script lang="ts">
import { mdiFileCodeOutline, mdiFolderArrowUpOutline, mdiFolderOpenOutline, mdiFolderOutline } from '@mdi/js'
import { onMount, tick } from 'svelte'
import { onMount } from 'svelte'
import type { TreeEntryFields } from '@sourcegraph/shared/src/graphql-operations'
import { afterNavigate, goto } from '$app/navigation'
import Icon from '$lib/Icon.svelte'
import { type FileTreeProvider, NODE_LIMIT } from '$lib/repo/api/tree'
import { type FileTreeProvider, NODE_LIMIT, type FileTreeNodeValue } from '$lib/repo/api/tree'
import { getSidebarFileTreeStateForRepo } from '$lib/repo/stores'
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
import { createForwardStore } from '$lib/utils'
@ -82,7 +82,7 @@
treeView.scrollSelectedItemIntoView()
}
let treeView: TreeView<FileTreeProvider>
let treeView: TreeView<FileTreeNodeValue>
let repoName = treeProvider.getRepoName()
// Since context is only set once when the component is created
// we need to dynamically sync any changes to the corresponding

View File

@ -1,39 +1,40 @@
<script lang="ts">
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: branches = asStore(data.branches.deferred)
$: defaultBranch = !$branches.loading && $branches.data ? $branches.data.defaultBranch : null
$: activeBranches = !$branches.loading && $branches.data ? $branches.data.activeBranches : null
const { pending, value: branches, set } = createPromiseStore<typeof data.branches.deferred>()
$: set(data.branches.deferred)
$: defaultBranch = $branches?.defaultBranch
$: activeBranches = $branches?.activeBranches
</script>
{#if $branches.loading}
{#if $pending}
<LoadingSpinner />
{:else if $branches.data}
{#if defaultBranch}
<table class="mb-3">
<thead><tr><th colspan="3">Default branch</th></tr></thead>
<tbody>
<GitReference ref={defaultBranch} />
</tbody>
</table>
{/if}
{/if}
{#if activeBranches && activeBranches.length > 0}
<table>
<thead><tr><th colspan="3">Active branches</th></tr></thead>
<tbody>
{#each activeBranches as branch (branch.id)}
<GitReference ref={branch} />
{/each}
</tbody>
</table>
{/if}
{#if defaultBranch}
<table class="mb-3">
<thead><tr><th colspan="3">Default branch</th></tr></thead>
<tbody>
<GitReference ref={defaultBranch} />
</tbody>
</table>
{/if}
{#if activeBranches && activeBranches.length > 0}
<table>
<thead><tr><th colspan="3">Active branches</th></tr></thead>
<tbody>
{#each activeBranches as branch (branch.id)}
<GitReference ref={branch} />
{/each}
</tbody>
</table>
{/if}
<style lang="scss">

View File

@ -1,18 +1,19 @@
<script lang="ts">
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: branches = asStore(data.branches.deferred)
$: nodes = !$branches.loading && $branches.data ? $branches.data.nodes : null
$: total = !$branches.loading && $branches.data ? $branches.data.totalCount : null
const { pending, value: branches, set } = createPromiseStore<typeof data.branches.deferred>()
$: set(data.branches.deferred)
$: nodes = $branches?.nodes
$: total = $branches?.totalCount
</script>
{#if $branches.loading}
{#if $pending}
<LoadingSpinner />
{:else if nodes}
<!-- TODO: Search input to filter branches by name -->

View File

@ -1,40 +1,43 @@
<script lang="ts">
import Commit from '$lib/Commit.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
import FileDiff from './FileDiff.svelte'
export let data: PageData
$: commit = asStore(data.commit.deferred)
$: diff = asStore(data.diff.deferred)
const { pending: commitPending, value: commit, set: setCommit } = createPromiseStore<typeof data.commit.deferred>()
$: setCommit(data.commit.deferred)
const { pending: diffPending, value: diff, set: setDiff } = createPromiseStore<typeof data.diff.deferred>()
$: setDiff(data.diff.deferred)
$: pending = $diffPending || $commitPending
</script>
<section>
{#if !$commit.loading && $commit.data}
{#if $commit}
<div class="header">
<div class="info"><Commit commit={$commit.data} alwaysExpanded /></div>
<div class="info"><Commit commit={$commit} alwaysExpanded /></div>
<div>
<span>Commit:&nbsp;{$commit.data.abbreviatedOID}</span>
<span>Commit:&nbsp;{$commit.abbreviatedOID}</span>
<span class="parents">
{$commit.data.parents.length} parents:
{#each $commit.data.parents as parent}
{$commit.parents.length} parents:
{#each $commit.parents as parent}
<a href={parent.url}>{parent.abbreviatedOID}</a>{' '}
{/each}
</span>
</div>
</div>
{#if !$diff.loading && $diff.data}
{#if $diff}
<ul>
{#each $diff.data.nodes as node}
{#each $diff.nodes as node}
<li><FileDiff fileDiff={node} /></li>
{/each}
</ul>
{/if}
{/if}
{#if $commit.loading || $diff.loading}
{#if pending}
<LoadingSpinner />
{/if}
</section>

View File

@ -1,23 +1,24 @@
<script lang="ts">
import Commit from '$lib/Commit.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: commits = asStore(data.commits.deferred)
const { pending, value: commits, set } = createPromiseStore<typeof data.commits.deferred>()
$: set(data.commits.deferred)
</script>
<section>
<div>
<h2>View commits from this repsitory</h2>
<h3>Changes</h3>
{#if $commits.loading}
{#if $pending}
Loading...
{:else if $commits.data}
{:else if $commits}
<ul>
{#each $commits.data as commit (commit.url)}
{#each $commits as commit (commit.url)}
<li><Commit {commit} /></li>
{/each}
</ul>

View File

@ -3,10 +3,9 @@
import { page } from '$app/stores'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import Paginator from '$lib/Paginator.svelte'
import { getRelativeTime } from '$lib/relativeTime'
import { currentDate } from '$lib/stores'
import UserAvatar from '$lib/UserAvatar.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore, getRelativeTime } from '$lib/utils'
import { Button, ButtonGroup } from '$lib/wildcard'
import type { PageData } from './$types'
@ -20,20 +19,26 @@
['All time', ''],
]
$: timePeriod = data.after
$: contributorsLoader = asStore(data.deferred.contributors)
$: loading = $contributorsLoader.loading
let connection: Extract<typeof $contributorsLoader, { loading: false }>['data']
$: if (!$contributorsLoader.loading && $contributorsLoader.data) {
connection = $contributorsLoader.data
const { pending, latestValue: contributors, set } = createPromiseStore<typeof data.deferred.contributors>()
$: set(data.deferred.contributors)
// We want to show stale contributors data when the user navigates to
// the next or previous page for the current time period. When the user
// changes the time period we want to show a loading indicator instead.
let currentContributors = $contributors
$: if (!$pending && $contributors) {
currentContributors = $contributors
}
$: timePeriod = data.after
async function setTimePeriod(event: MouseEvent) {
const element = event.target as HTMLButtonElement
timePeriod = element.dataset.value ?? ''
const newURL = new URL($page.url)
newURL.search = timePeriod ? `after=${timePeriod}` : ''
connection = null
// Don't show stale contributors when switching the time period
currentContributors = null
await goto(newURL)
}
</script>
@ -58,12 +63,12 @@
{/each}
</ButtonGroup>
</form>
{#if !connection && loading}
{#if !currentContributors && $pending}
<div class="mt-3">
<LoadingSpinner />
</div>
{:else if connection}
{@const nodes = connection.nodes}
{:else if currentContributors}
{@const nodes = currentContributors.nodes}
<table class="mt-3">
<tbody>
{#each nodes as contributor}
@ -84,9 +89,9 @@
</tbody>
</table>
<div class="d-flex flex-column align-items-center">
<Paginator disabled={loading} pageInfo={connection.pageInfo} />
<Paginator disabled={$pending} pageInfo={currentContributors.pageInfo} />
<p class="mt-1 text-muted">
<small>Total contributors: {connection.totalCount}</small>
<small>Total contributors: {currentContributors.totalCount}</small>
</p>
</div>
{/if}

View File

@ -1,20 +1,22 @@
<script lang="ts">
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { asStore } from '$lib/utils'
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
export let data: PageData
$: tags = asStore(data.tags.deferred)
$: nodes = !$tags.loading && $tags.data ? $tags.data.nodes : null
$: total = !$tags.loading && $tags.data ? $tags.data.totalCount : null
const { pending, value: tags, set } = createPromiseStore<typeof data.tags.deferred>()
$: set(data.tags.deferred)
$: nodes = $tags?.nodes
$: total = $tags?.totalCount
</script>
<section>
<div>
{#if $tags.loading}
{#if $pending}
<LoadingSpinner />
{:else if nodes}
<!-- TODO: Search input to filter tags by name -->

View File

@ -1,12 +1,15 @@
import { sveltekit } from '@sveltejs/kit/vite'
import type { UserConfig } from 'vite'
import { defineConfig } from 'vite'
const config: UserConfig = {
const config = defineConfig(({ mode }) => ({
plugins: [sveltekit()],
define: {
'process.platform': '"browser"',
'process.env': '{}',
},
define:
mode === 'test'
? {}
: {
'process.platform': '"browser"',
'process.env': '{}',
},
css: {
modules: {
localsConvention: 'camelCase',
@ -29,6 +32,6 @@ const config: UserConfig = {
'linguist-languages',
],
},
}
}))
export default config

View File

@ -1700,6 +1700,9 @@ importers:
specifier: ^1.29.0
version: 1.29.0
devDependencies:
'@faker-js/faker':
specifier: ^8.0.2
version: 8.0.2
'@playwright/test':
specifier: 1.25.0
version: 1.25.0
@ -1772,6 +1775,9 @@ importers:
vite:
specifier: ^4.3.9
version: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
vitest:
specifier: ^0.33.0
version: 0.33.0(jsdom@16.7.0)(sass@1.32.4)
client/wildcard:
dependencies:
@ -5611,6 +5617,11 @@ packages:
resolution: {integrity: sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
/@faker-js/faker@8.0.2:
resolution: {integrity: sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
dev: true
/@fal-works/esbuild-plugin-global-externals@2.1.2:
resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==}
dev: true
@ -11490,7 +11501,7 @@ packages:
'@storybook/node-logger': 7.0.26
'@storybook/svelte': 7.0.26(svelte@4.0.0)
'@sveltejs/vite-plugin-svelte': 2.4.2(svelte@4.0.0)(vite@4.3.9)
magic-string: 0.30.0
magic-string: 0.30.1
svelte: 4.0.0
sveltedoc-parser: 4.2.1
ts-dedent: 2.2.0
@ -11715,7 +11726,7 @@ packages:
devalue: 4.3.2
esm-env: 1.0.0
kleur: 4.1.5
magic-string: 0.30.0
magic-string: 0.30.1
mime: 3.0.0
sade: 1.8.1
set-cookie-parser: 2.6.0
@ -11754,7 +11765,7 @@ packages:
debug: 4.3.4
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.0
magic-string: 0.30.1
svelte: 4.0.0
svelte-hmr: 0.15.2(svelte@4.0.0)
vite: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
@ -12080,6 +12091,16 @@ packages:
- webpack-cli
dev: true
/@types/chai-subset@1.3.3:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
'@types/chai': 4.3.5
dev: true
/@types/chai@4.3.5:
resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==}
dev: true
/@types/chrome@0.0.106:
resolution: {integrity: sha512-sCQa+QJL/Kl+6ngU4xD1VjTFIkQDizOHN94zbb7byK9D3U79x/KXjqXyS7lHoa2q9xw3bWePb+giLhBKOILRuA==}
dependencies:
@ -13281,7 +13302,7 @@ packages:
find-up: 5.0.0
javascript-stringify: 2.1.0
lodash: 4.17.21
mlly: 1.3.0
mlly: 1.4.0
outdent: 0.8.0
vite: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
vite-node: 0.28.5(@types/node@13.13.5)(sass@1.32.4)
@ -13510,6 +13531,44 @@ packages:
- supports-color
dev: true
/@vitest/expect@0.33.0:
resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==}
dependencies:
'@vitest/spy': 0.33.0
'@vitest/utils': 0.33.0
chai: 4.3.7
dev: true
/@vitest/runner@0.33.0:
resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==}
dependencies:
'@vitest/utils': 0.33.0
p-limit: 4.0.0
pathe: 1.1.1
dev: true
/@vitest/snapshot@0.33.0:
resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==}
dependencies:
magic-string: 0.30.1
pathe: 1.1.1
pretty-format: 29.5.0
dev: true
/@vitest/spy@0.33.0:
resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==}
dependencies:
tinyspy: 2.1.1
dev: true
/@vitest/utils@0.33.0:
resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==}
dependencies:
diff-sequences: 29.4.3
loupe: 2.3.6
pretty-format: 29.5.0
dev: true
/@vscode/codicons@0.0.29:
resolution: {integrity: sha512-AXhTv1nl3r4W5DqAfXXKiawQNW+tLBNlXn/GcsnFCL0j17sQ2AY+az9oB9K6wjkibq1fndNJvmT8RYN712Fdww==}
dev: false
@ -13886,12 +13945,12 @@ packages:
acorn: 7.4.1
acorn-walk: 7.2.0
/acorn-import-assertions@1.8.0(acorn@8.8.2):
/acorn-import-assertions@1.8.0(acorn@8.10.0):
resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==}
peerDependencies:
acorn: ^8
dependencies:
acorn: 8.8.2
acorn: 8.10.0
/acorn-jsx@5.3.2(acorn@7.4.1):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -13901,12 +13960,12 @@ packages:
acorn: 7.4.1
dev: true
/acorn-jsx@5.3.2(acorn@8.8.2):
/acorn-jsx@5.3.2(acorn@8.10.0):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.8.2
acorn: 8.10.0
/acorn-walk@7.2.0:
resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
@ -13921,8 +13980,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
/acorn@8.8.2:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
/acorn@8.10.0:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
engines: {node: '>=0.4.0'}
hasBin: true
@ -14430,6 +14489,10 @@ packages:
util: 0.12.5
dev: true
/assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/assign-symbols@1.0.0:
resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
engines: {node: '>=0.10.0'}
@ -15668,6 +15731,19 @@ packages:
resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
dev: true
/chai@4.3.7:
resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==}
engines: {node: '>=4'}
dependencies:
assertion-error: 1.1.0
check-error: 1.0.2
deep-eql: 4.1.3
get-func-name: 2.0.0
loupe: 2.3.6
pathval: 1.1.1
type-detect: 4.0.8
dev: true
/chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@ -15782,6 +15858,10 @@ packages:
/charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
/check-error@1.0.2:
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
dev: true
/cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
@ -16195,7 +16275,7 @@ packages:
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
'@types/estree': 1.0.0
acorn: 8.8.2
acorn: 8.10.0
estree-walker: 3.0.3
periscopic: 3.1.0
dev: true
@ -17744,6 +17824,13 @@ packages:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
dev: true
/deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
engines: {node: '>=6'}
dependencies:
type-detect: 4.0.8
dev: true
/deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@ -18022,6 +18109,11 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
dev: true
/diff-sequences@29.4.3:
resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dev: true
/diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
@ -19251,8 +19343,8 @@ packages:
resolution: {integrity: sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.8.2
acorn-jsx: 5.3.2(acorn@8.8.2)
acorn: 8.10.0
acorn-jsx: 5.3.2(acorn@8.10.0)
eslint-visitor-keys: 3.4.1
dev: true
@ -19260,8 +19352,8 @@ packages:
resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.8.2
acorn-jsx: 5.3.2(acorn@8.8.2)
acorn: 8.10.0
acorn-jsx: 5.3.2(acorn@8.10.0)
eslint-visitor-keys: 3.4.1
/esprima@4.0.1:
@ -20365,6 +20457,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
/get-func-name@2.0.0:
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
dev: true
/get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies:
@ -23725,7 +23821,7 @@ packages:
optional: true
dependencies:
abab: 2.0.6
acorn: 8.8.2
acorn: 8.10.0
acorn-globals: 6.0.0
cssom: 0.4.4
cssstyle: 2.3.0
@ -23767,7 +23863,7 @@ packages:
optional: true
dependencies:
abab: 2.0.6
acorn: 8.8.2
acorn: 8.10.0
acorn-globals: 6.0.0
cssom: 0.5.0
cssstyle: 2.3.0
@ -24375,6 +24471,11 @@ packages:
resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==}
engines: {node: '>= 12.13.0'}
/local-pkg@0.4.3:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
engines: {node: '>=14'}
dev: true
/locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
dev: true
@ -24615,6 +24716,12 @@ packages:
signal-exit: 3.0.7
dev: true
/loupe@2.3.6:
resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==}
dependencies:
get-func-name: 2.0.0
dev: true
/lower-case-first@2.0.2:
resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==}
dependencies:
@ -24700,8 +24807,8 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/magic-string@0.30.0:
resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
/magic-string@0.30.1:
resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
@ -25718,8 +25825,8 @@ packages:
/micromark-extension-mdxjs@1.0.1:
resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==}
dependencies:
acorn: 8.8.2
acorn-jsx: 5.3.2(acorn@8.8.2)
acorn: 8.10.0
acorn-jsx: 5.3.2(acorn@8.10.0)
micromark-extension-mdx-expression: 1.0.8
micromark-extension-mdx-jsx: 1.0.5
micromark-extension-mdx-md: 1.0.1
@ -26100,10 +26207,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
/mlly@1.3.0:
resolution: {integrity: sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==}
/mlly@1.4.0:
resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==}
dependencies:
acorn: 8.8.2
acorn: 8.10.0
pathe: 1.1.1
pkg-types: 1.0.3
ufo: 1.1.2
@ -27091,6 +27198,13 @@ packages:
dependencies:
yocto-queue: 0.1.0
/p-limit@4.0.0:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
yocto-queue: 1.0.0
dev: true
/p-locate@2.0.0:
resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==}
engines: {node: '>=4'}
@ -27452,6 +27566,10 @@ packages:
/pathe@1.1.1:
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
/pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true
/peek-stream@1.1.3:
resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==}
dependencies:
@ -27555,7 +27673,7 @@ packages:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
dependencies:
jsonc-parser: 3.2.0
mlly: 1.3.0
mlly: 1.4.0
pathe: 1.1.1
/playwright-core@1.25.0:
@ -28420,7 +28538,6 @@ packages:
'@jest/schemas': 29.6.0
ansi-styles: 5.2.0
react-is: 18.2.0
dev: false
/pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
@ -30620,6 +30737,10 @@ packages:
get-intrinsic: 1.2.1
object-inspect: 1.12.3
/siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
dev: true
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@ -31194,6 +31315,10 @@ packages:
dependencies:
escape-string-regexp: 2.0.0
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: true
/stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
@ -31223,6 +31348,10 @@ packages:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
/std-env@3.3.3:
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
dev: true
/stdin@0.0.1:
resolution: {integrity: sha512-2bacd1TXzqOEsqRa+eEWkRdOSznwptrs4gqFcpMq5tOtmJUGPZd10W5Lam6wQ4YQ/+qjQt4e9u35yXCF6mrlfQ==}
dev: true
@ -31542,6 +31671,12 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
/strip-literal@1.0.1:
resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==}
dependencies:
acorn: 8.10.0
dev: true
/strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
dev: false
@ -31900,7 +32035,7 @@ packages:
'@ampproject/remapping': 2.2.1
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.18
acorn: 8.8.2
acorn: 8.10.0
aria-query: 5.3.0
axobject-query: 3.2.1
code-red: 1.0.3
@ -31908,7 +32043,7 @@ packages:
estree-walker: 3.0.3
is-reference: 3.0.1
locate-character: 3.0.0
magic-string: 0.30.0
magic-string: 0.30.1
periscopic: 3.1.0
dev: true
@ -32170,7 +32305,7 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
acorn: 8.8.2
acorn: 8.10.0
commander: 2.20.3
source-map: 0.6.1
source-map-support: 0.5.21
@ -32182,7 +32317,7 @@ packages:
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.3
acorn: 8.8.2
acorn: 8.10.0
commander: 2.20.3
source-map-support: 0.5.21
@ -32275,6 +32410,20 @@ packages:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false
/tinybench@2.5.0:
resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==}
dev: true
/tinypool@0.6.0:
resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==}
engines: {node: '>=14.0.0'}
dev: true
/tinyspy@2.1.1:
resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==}
engines: {node: '>=14.0.0'}
dev: true
/title-case@3.0.3:
resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==}
dependencies:
@ -32509,7 +32658,7 @@ packages:
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 13.13.5
acorn: 8.8.2
acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.0
create-require: 1.1.1
@ -32539,7 +32688,7 @@ packages:
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 13.13.5
acorn: 8.8.2
acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.0
create-require: 1.1.1
@ -33135,7 +33284,7 @@ packages:
/unplugin@0.10.2:
resolution: {integrity: sha512-6rk7GUa4ICYjae5PrAllvcDeuT8pA9+j5J5EkxbMFaV+SalHhxZ7X2dohMzu6C3XzsMT+6jwR/+pwPNR3uK9MA==}
dependencies:
acorn: 8.8.2
acorn: 8.10.0
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.6
@ -33621,7 +33770,7 @@ packages:
dependencies:
cac: 6.7.14
debug: 4.3.4
mlly: 1.3.0
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
source-map: 0.6.1
@ -33636,6 +33785,27 @@ packages:
- supports-color
- terser
/vite-node@0.33.0(@types/node@13.13.5)(sass@1.32.4):
resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==}
engines: {node: '>=v14.18.0'}
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
vite: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
transitivePeerDependencies:
- '@types/node'
- less
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vite@4.3.9(@types/node@13.13.5)(sass@1.32.4):
resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -33680,6 +33850,71 @@ packages:
vite: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
dev: true
/vitest@0.33.0(jsdom@16.7.0)(sass@1.32.4):
resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==}
engines: {node: '>=v14.18.0'}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@vitest/browser': '*'
'@vitest/ui': '*'
happy-dom: '*'
jsdom: '*'
playwright: '*'
safaridriver: '*'
webdriverio: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
playwright:
optional: true
safaridriver:
optional: true
webdriverio:
optional: true
dependencies:
'@types/chai': 4.3.5
'@types/chai-subset': 1.3.3
'@types/node': 13.13.5
'@vitest/expect': 0.33.0
'@vitest/runner': 0.33.0
'@vitest/snapshot': 0.33.0
'@vitest/spy': 0.33.0
'@vitest/utils': 0.33.0
acorn: 8.10.0
acorn-walk: 8.2.0
cac: 6.7.14
chai: 4.3.7
debug: 4.3.4
jsdom: 16.7.0
local-pkg: 0.4.3
magic-string: 0.30.1
pathe: 1.1.1
picocolors: 1.0.0
std-env: 3.3.3
strip-literal: 1.0.1
tinybench: 2.5.0
tinypool: 0.6.0
vite: 4.3.9(@types/node@13.13.5)(sass@1.32.4)
vite-node: 0.33.0(@types/node@13.13.5)(sass@1.32.4)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vlq@1.0.1:
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
dev: false
@ -33690,7 +33925,7 @@ packages:
deprecated: The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.
hasBin: true
dependencies:
acorn: 8.8.2
acorn: 8.10.0
acorn-walk: 8.2.0
/vsce@2.7.0:
@ -33858,7 +34093,7 @@ packages:
engines: {node: '>= 10.13.0'}
hasBin: true
dependencies:
acorn: 8.8.2
acorn: 8.10.0
acorn-walk: 8.2.0
chalk: 4.1.2
commander: 7.2.0
@ -34089,8 +34324,8 @@ packages:
'@webassemblyjs/ast': 1.11.1
'@webassemblyjs/wasm-edit': 1.11.1
'@webassemblyjs/wasm-parser': 1.11.1
acorn: 8.8.2
acorn-import-assertions: 1.8.0(acorn@8.8.2)
acorn: 8.10.0
acorn-import-assertions: 1.8.0(acorn@8.10.0)
browserslist: 4.21.7
chrome-trace-event: 1.0.2
enhanced-resolve: 5.10.0
@ -34222,6 +34457,15 @@ packages:
dependencies:
isexe: 2.0.0
/why-is-node-running@2.2.2:
resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
engines: {node: '>=8'}
hasBin: true
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
dev: true
/wide-align@1.1.3:
resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==}
dependencies:
@ -34764,6 +35008,11 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: true
/zdog@1.1.3:
resolution: {integrity: sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==}
dev: false