sveltekit: React integration (#48269)

This PR adds various helpers to integrate existing React routes into the
SvelteKit prototype. The idea is that we create pages which handle a
complete route "namespace" (e.g. `/notebooks/*`) delegate navigation
within this namespace to the React component / React router.

In order to properly handle "cross framework links" (i.e. a link
rendered with SvelteKit pointing to a React page and vice versa), and to
make navigation state synchronization more robust in general I've
introduced a custom React Router router which integrates with
SvelteKit's router API.

This PR adds support for the `/notebooks` and `/insights` routes but I
could only really test Notebooks because insights isn't available on
sourcegraph.com (which the prototype proxies to).

## Test plan

Run the prototype and navigate to, from ans withing `/notebooks`.
This commit is contained in:
Felix Kling 2023-02-28 15:49:32 +01:00 committed by GitHub
parent ef37053587
commit dad459f92b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 422 additions and 48 deletions

View File

@ -1,13 +1,13 @@
export function isChrome(): boolean {
return !!window.navigator.userAgent.match(/chrome|chromium|crios/i)
return typeof window !== 'undefined' && !!window.navigator.userAgent.match(/chrome|chromium|crios/i)
}
export function isSafari(): boolean {
return !!window.navigator.userAgent.match(/safari/i) && !isChrome()
return typeof window !== 'undefined' && !!window.navigator.userAgent.match(/safari/i) && !isChrome()
}
export function isFirefox(): boolean {
return window.navigator.userAgent.includes('Firefox')
return typeof window !== 'undefined' && window.navigator.userAgent.includes('Firefox')
}
export function getBrowserName(): 'chrome' | 'safari' | 'firefox' | 'other' {
@ -19,5 +19,5 @@ export function getBrowserName(): 'chrome' | 'safari' | 'firefox' | 'other' {
* is accessed only when the function is called
*/
export function isMacPlatform(): boolean {
return window.navigator.platform.includes('Mac')
return typeof window !== 'undefined' && window.navigator.platform.includes('Mac')
}

View File

@ -12,3 +12,5 @@ package.json
pnpm-lock.yaml
package-lock.json
yarn.lock
tsconfig.json

View File

@ -17,21 +17,22 @@
},
"devDependencies": {
"@playwright/test": "1.25.0",
"@sveltejs/adapter-auto": "1.0.0-next.91",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.0",
"@sveltejs/kit": "^1.3.2",
"@sveltejs/kit": "^1.5.6",
"@types/cookie": "^0.5.1",
"@types/prismjs": "^1.26.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier-plugin-svelte": "^2.9.0",
"svelte": "^3.55.1",
"svelte-check": "^3.0.3",
"svelte-check": "^3.0.4",
"tslib": "2.1.0",
"vite": "^4.0.4"
"vite": "^4.1.4"
},
"type": "module",
"dependencies": {
"@popperjs/core": "^2.11.6",
"@remix-run/router": "^1.3.2",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",

View File

@ -0,0 +1,38 @@
<script lang="ts">
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { onDestroy, onMount } from 'svelte'
import { ReactAdapter } from './react-interop'
type ComponentProps = $$Generic<{}>
export let component: React.FunctionComponent<ComponentProps>
export let props: ComponentProps
export let route: string
let container: HTMLDivElement
let root: Root | null = null
function renderComponent(
root: Root | null,
component: React.FunctionComponent<ComponentProps>,
props: ComponentProps,
route: string
) {
root?.render(
React.createElement(
ReactAdapter,
{
route,
},
React.createElement(component, props)
)
)
}
onMount(() => (root = createRoot(container)))
onDestroy(() => root?.unmount())
$: renderComponent(root, component, props, route)
</script>
<div bind:this={container} />

View File

@ -8,6 +8,8 @@ import { eventLogger, type EventLogger } from '@sourcegraph/web/src/tracking/eve
import { PUBLIC_ENABLE_EVENT_LOGGER } from '$env/static/public'
export { eventLogger }
/**
* Can only be called during component initialization. It logs a view event when
* the component is mounted (and event logging is enabled).

View File

@ -0,0 +1,195 @@
import React, { useEffect, useMemo, type FC, type PropsWithChildren } from 'react'
import { createRouter, type History, Action, type Location, type Router } from '@remix-run/router'
import type { Navigation } from '@sveltejs/kit'
import { RouterProvider, UNSAFE_enhanceManualRouteObjects, type RouteObject } from 'react-router-dom'
import { RouterLink, setLinkComponent } from 'wildcard/src'
import { WildcardThemeContext, type WildcardTheme } from './wildcard'
import { goto } from '$app/navigation'
import { navigating } from '$app/stores'
setLinkComponent(RouterLink)
const WILDCARD_THEME: WildcardTheme = {
isBranded: true,
}
/**
* Creates a minimal context for rendering React components inside Svelte, including a
* custom React Router router to integrate with SvelteKit.
*/
export const ReactAdapter: FC<PropsWithChildren<{ route: string }>> = ({ route, children }) => {
const router = useMemo(
() =>
createSvelteKitRouter([
{
path: route,
// React.Suspense seems necessary to render the components without error
element: <React.Suspense fallback={true}>{children}</React.Suspense>,
},
]),
[route, children]
)
// Dispose is marked as INTERNAL but without calling it the listeners created by React
// Router are not removed. It doesn't look like React Router expects to be unmounted
// during the lifetime of the application.
useEffect(() => () => router.dispose(), [router])
return (
<WildcardThemeContext.Provider value={WILDCARD_THEME}>
<RouterProvider router={router} />
</WildcardThemeContext.Provider>
)
}
/**
* Custom router that synchronizes between the SvelteKit routing and React router.
*/
function createSvelteKitRouter(routes: RouteObject[]): Router {
return createRouter({
routes: UNSAFE_enhanceManualRouteObjects(routes),
history: createSvelteKitHistory(),
}).initialize()
}
/**
* Custom history that synchronizes between the SvelteKit routing and React router.
* This is a "best effort" implementation because the API used here is not very well
* documented or doesn't seem to be intended for this use case.
*
* Caveat: Using the browser back/forward buttons to navigate between hash targets on the
* same page doesn't seem to scroll the target into view. It's not clear yet why that is.
*/
function createSvelteKitHistory(): History {
let action: Action = Action.Push
const history: History = {
get action() {
return action
},
get location() {
return createLocation(window.location)
},
createURL,
createHref(to) {
return typeof to === 'string' ? to : toPath(createURL(to))
},
go(delta) {
window.history.go(delta)
},
push(to, state) {
action = Action.Push
// Without this, links that are outside of the React component (i.e. Svelte)
// pointing to a path handle by the React component causes duplicate entries
// See below for more information.
// This is safe to do because the browser does the same when clicking on a
// link that navigates to the curren page.
if (createURL(to).href !== window.location.href) {
goto(createURL(to), { state: state ?? undefined })
// Make eslint happy
.catch(() => {})
}
},
replace(to, state) {
action = Action.Replace
goto(createURL(to), { state: state ?? undefined, replaceState: true })
// Make eslint happy
.catch(() => {})
},
encodeLocation(to) {
const url = createURL(to)
return {
hash: url.hash,
search: url.search,
pathname: url.pathname,
}
},
listen(listener) {
let prevState: Navigation | null = null
return navigating.subscribe(state => {
// Events are emitted when navigation *starts*. That means the browser URL hasn't updated yet
// I don't know whether that's relevant for React Router or not, but in order to make the
// equality check in the `push` method possible we need to wait to call `listener` until the
// navigation completed.
// This is done by storing the emitted value in `prevState` and wait until the next event, which
// will emit `null`.
// NOTE: SvelteKit does not emit events when the back/forward buttons are used to navigate between
// "hashes" on the same page. SvelteKit instead lets the browser handle these natively. However
// I noticed that at least on Notebook pages this won't scroll the target into view.
if (!state && prevState) {
switch (prevState.type) {
case 'popstate':
action = Action.Pop
if (prevState.to) {
listener({
action,
location: createLocation(prevState.to.url),
delta: prevState.delta ?? 0,
})
}
break
case 'link':
// This is a special case for SvelteKit. In a normal browser context it seems that `listen`
// should only handle popstate events. Listening to the SvelteKit 'link' event seems
// necessary to properly handle SvelteKit links which point to paths handled by this React
// component (it neither works without it nor with the default browser router).
// However, React Router doesn't seem to expect that `listener` can be called for "push" events
// and will subequently call `history.push`. In order to prevent a double history entry
// we are checking whether the target URL is the same as the current URL and do not call
// back to SvelteKit again if that's the case.
action = Action.Push
if (prevState.to) {
listener({ action, location: createLocation(prevState.to.url), delta: 1 })
}
break
}
}
prevState = state
})
},
}
return history
}
function createURL(path: string | { pathname?: string; search?: string; hash?: string }): URL {
if (typeof path === 'string') {
return new URL(path, window.location.href)
}
const url = new URL(window.location.href)
if (path.pathname !== undefined) {
url.pathname = path.pathname
}
if (path.search !== undefined) {
url.search = path.search
}
if (path.hash !== undefined) {
url.hash = path.hash
}
return url
}
function createLocation(target: URL | typeof window['location']): Location {
return {
pathname: target.pathname,
search: target.search,
hash: target.hash,
state: window.history.state?.usr ?? null,
key: window.history.state?.key ?? 'default',
}
}
function toPath(location: { pathname: string; search: string; hash: string }): string {
return location.pathname + location.search + location.hash
}

View File

@ -35,7 +35,6 @@ export { filterExists } from '@sourcegraph/shared/src/search/query/validate'
export { FilterType } from '@sourcegraph/shared/src/search/query/filters'
export { getGlobalSearchContextFilter } from '@sourcegraph/shared/src/search/query/query'
export { omitFilter } from '@sourcegraph/shared/src/search/query/transformer'
export { observeSystemIsLightTheme } from '@sourcegraph/shared/src/theme'
export type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
export { type SettingsCascade, type SettingsSubject, gqlToCascade } from '@sourcegraph/shared/src/settings/settings'
export { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestions'
@ -43,6 +42,7 @@ export { QueryChangeSource, type QueryState } from '@sourcegraph/shared/src/sear
export { migrateLocalStorageToTemporarySettings } from '@sourcegraph/shared/src/settings/temporary/migrateLocalStorageToTemporarySettings'
export type { TemporarySettings } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings'
export { SyntaxKind } from '@sourcegraph/shared/src/codeintel/scip'
export { type FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
// Copies of non-reusable code

View File

@ -16,6 +16,12 @@ export { replaceRevisionInURL } from '@sourcegraph/web/src/util/url'
export type { ResolvedRevision } from '@sourcegraph/web/src/repo/backend'
export { syntaxHighlight } from '@sourcegraph/web/src/repo/blob/codemirror/highlight'
export { defaultSearchModeFromSettings } from '@sourcegraph/web/src/util/settings'
export { setExperimentalFeaturesFromSettings } from '@sourcegraph/web/src/stores/experimentalFeatures'
export { GlobalNotebooksArea, type GlobalNotebooksAreaProps } from '@sourcegraph/web/src/notebooks/GlobalNotebooksArea'
export {
CodeInsightsRouter,
type CodeInsightsRouterProps,
} from '@sourcegraph/web/src/enterprise/insights/CodeInsightsRouter'
// Copy of non-reusable code

View File

@ -1,2 +1,6 @@
// We want to limit the number of imported modules as much as possible
/* eslint-disable no-restricted-imports */
export { default as Button } from './Button.svelte'
export { default as ButtonGroup } from './ButtonGroup.svelte'
export { WildcardThemeContext, type WildcardTheme } from '@sourcegraph/wildcard/src/hooks/useWildcardTheme'

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { CodeInsightsRouter, type CodeInsightsRouterProps } from '$lib/web'
import { PUBLIC_DOTCOM } from '$env/static/public'
import type { PageData } from './$types'
import ReactComponent from '$lib/ReactComponent.svelte'
import { eventLogger } from '$lib/logger'
export let data: PageData
const isSourcegraphDotCom = !!PUBLIC_DOTCOM
$: props = {
telemetryService: eventLogger,
isSourcegraphDotCom,
authenticatedUser: data.user,
} satisfies CodeInsightsRouterProps
</script>
<ReactComponent route="/insights/*" component={CodeInsightsRouter} {props} />

View File

@ -0,0 +1,46 @@
<!--
This page is rendered by using the corresponding React component.
CAVEAT: Using back/forward buttons to navigate between headings inside a
Notebook (has navigation) doesn't scroll the target into view.
We need to investigate if that's a fundamental problem with
SvelteKit<->ReactRouter of if Notebooks are doing something that prevents
the default browser behavior to take place.
-->
<script lang="ts">
import { PUBLIC_DOTCOM } from '$env/static/public'
import { GlobalNotebooksArea, type GlobalNotebooksAreaProps } from '$lib/web'
import type { PageData } from './$types'
import {
aggregateStreamingSearch,
fetchHighlightedFileLineRanges as _fetchHighlightedFileLineRanges,
type FetchFileParameters,
} from '$lib/shared'
import type { Observable } from 'rxjs'
import ReactComponent from '$lib/ReactComponent.svelte'
import { eventLogger } from '$lib/logger'
export let data: PageData
const isSourcegraphDotCom = !!PUBLIC_DOTCOM
function fetchHighlightedFileLineRanges(parameters: FetchFileParameters, force?: boolean): Observable<string[][]> {
return _fetchHighlightedFileLineRanges({ ...parameters, platformContext: data.platformContext }, force)
}
$: props = {
fetchHighlightedFileLineRanges,
telemetryService: eventLogger,
isSourcegraphDotCom,
// FIXME: Terrible hack to avoid having to create a complete context object
platformContext: data.platformContext as any,
authenticatedUser: data.user,
notebooksEnabled: true,
globbing: false,
settingsCascade: data.settings,
searchContextsEnabled: false,
streamSearch: aggregateStreamingSearch,
} satisfies GlobalNotebooksAreaProps
</script>
<ReactComponent route="/notebooks/*" component={GlobalNotebooksArea} {props} />

View File

@ -1,24 +1,39 @@
<script lang="ts">
import { onMount, setContext } from 'svelte'
import { readable, writable } from 'svelte/store'
import { readable, writable, type Readable } from 'svelte/store'
import { browser } from '$app/environment'
import { KEY, type SourcegraphContext } from '$lib/stores'
import { isErrorLike } from '$lib/common'
import { observeSystemIsLightTheme, TemporarySettingsStorage } from '$lib/shared'
import { readableObservable } from '$lib/utils'
import { TemporarySettingsStorage } from '$lib/shared'
import { createTemporarySettingsStorage } from '$lib/temporarySettings'
import Header from './Header.svelte'
import './styles.scss'
import type { LayoutData } from './$types'
import type { LayoutData, Snapshot } from './$types'
import { setExperimentalFeaturesFromSettings } from '$lib/web'
import { beforeNavigate } from '$app/navigation'
export let data: LayoutData
function createLightThemeStore(): Readable<boolean> {
if (browser) {
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)')
return readable(!matchMedia.matches, set => {
const listener = (event: MediaQueryListEventMap['change']) => {
set(!event.matches)
}
matchMedia.addEventListener('change', listener)
return () => matchMedia.removeEventListener('change', listener)
})
}
return readable(true)
}
const user = writable(data.user ?? null)
const settings = writable(data.settings)
const settings = writable(isErrorLike(data.settings) ? null : data.settings.final)
const platformContext = writable(data.platformContext)
const isLightTheme = browser ? readableObservable(observeSystemIsLightTheme(window).observable) : readable(true)
const isLightTheme = createLightThemeStore()
// It's OK to set the temporary storage during initialization time because
// sign-in/out currently performs a full page refresh
const temporarySettingsStorage = createTemporarySettingsStorage(
@ -43,13 +58,44 @@
})
$: $user = data.user ?? null
$: $settings = data.settings
$: $settings = isErrorLike(data.settings) ? null : data.settings.final
$: $platformContext = data.platformContext
// Sync React stores
$: setExperimentalFeaturesFromSettings(data.settings)
$: if (browser) {
document.documentElement.classList.toggle('theme-light', $isLightTheme)
document.documentElement.classList.toggle('theme-dark', !$isLightTheme)
}
let main: HTMLElement | null = null
let scrollTop = 0
beforeNavigate(() => {
// It looks like `snapshot.capture` is called "too late", i.e. after the
// content has been updated. beforeNavigate is used to capture the correct
// scroll offset
scrollTop = main?.scrollTop ?? 0
})
export const snapshot: Snapshot<{ x: number }> = {
capture() {
return { x: scrollTop }
},
restore(value) {
restoreScrollPosition(value.x)
},
}
function restoreScrollPosition(y: number) {
const start = Date.now()
requestAnimationFrame(function scroll() {
if (main) {
main.scrollTo(0, y)
}
if ((!main || main.scrollTop !== y) && Date.now() - start < 3000) {
requestAnimationFrame(scroll)
}
})
}
</script>
<svelte:head>
@ -60,12 +106,12 @@
<div class="app">
<Header authenticatedUser={$user} />
<main>
<main bind:this={main}>
<slot />
</main>
</div>
<style>
<style lang="scss">
.app {
display: flex;
flex-direction: column;
@ -78,6 +124,6 @@
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
overflow: auto;
}
</style>

View File

@ -1,10 +1,9 @@
import { from } from 'rxjs'
import { map, switchMap, take } from 'rxjs/operators'
import { switchMap, take } from 'rxjs/operators'
import type { LayoutLoad } from './$types'
import { browser } from '$app/environment'
import { isErrorLike } from '$lib/common'
import { createPlatformContext } from '$lib/context'
import type { CurrentAuthStateResult } from '$lib/graphql/shared'
import { getDocumentNode } from '$lib/http-client'
@ -39,7 +38,6 @@ export const load: LayoutLoad = () => {
settings: from(platformContext)
.pipe(
switchMap(platformContext => platformContext.settings),
map(settingsOrError => (isErrorLike(settingsOrError.final) ? null : settingsOrError.final)),
take(1)
)
.toPromise(),

View File

@ -17,6 +17,8 @@
<nav class="ml-2">
<ul>
<HeaderNavLink href="/search" svgIconPath={mdiMagnify}>Code search</HeaderNavLink>
<HeaderNavLink href="/notebooks" svgIconPath={mdiBookOutline}>Notebooks</HeaderNavLink>
<HeaderNavLink href="/insights" svgIconPath={mdiChartBar}>Insights</HeaderNavLink>
</ul>
</nav>
<div class="user">

View File

@ -33,6 +33,12 @@ const config = {
'@apollo/client': '../../node_modules/@apollo/client/index.js',
lodash: './node_modules/lodash-es',
},
typescript: {
config: config => {
config.extends = '../../../tsconfig.base.json'
config.include = [...(config.include ?? []), '../src/**/*.tsx']
},
},
},
}

View File

@ -9,6 +9,7 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"jsx": "react",
},
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//

View File

@ -23,6 +23,12 @@ const config: UserConfig = {
},
},
},
optimizeDeps: {
exclude: [
// Without addings this Vite throws an error
'linguist-languages',
],
},
}
export default config

View File

@ -26,7 +26,7 @@ export interface GlobalNotebooksAreaProps
SettingsCascadeProps,
NotebookProps,
SearchStreamingProps,
SearchContextProps {
Pick<SearchContextProps, 'searchContextsEnabled'> {
authenticatedUser: AuthenticatedUser | null
isSourcegraphDotCom: boolean
fetchHighlightedFileLineRanges: (parameters: FetchFileParameters, force?: boolean) => Observable<string[][]>

View File

@ -1141,15 +1141,16 @@ importers:
specifiers:
'@playwright/test': 1.25.0
'@popperjs/core': ^2.11.6
'@remix-run/router': ^1.3.2
'@sourcegraph/branded': workspace:*
'@sourcegraph/common': workspace:*
'@sourcegraph/http-client': workspace:*
'@sourcegraph/shared': workspace:*
'@sourcegraph/web': workspace:*
'@sourcegraph/wildcard': workspace:*
'@sveltejs/adapter-auto': 1.0.0-next.91
'@sveltejs/adapter-auto': ^2.0.0
'@sveltejs/adapter-static': ^2.0.0
'@sveltejs/kit': ^1.3.2
'@sveltejs/kit': ^1.5.6
'@types/cookie': ^0.5.1
'@types/prismjs': ^1.26.0
eslint-plugin-svelte3: ^4.0.0
@ -1157,11 +1158,12 @@ importers:
prettier-plugin-svelte: ^2.9.0
prismjs: ^1.29.0
svelte: ^3.55.1
svelte-check: ^3.0.3
svelte-check: ^3.0.4
tslib: 2.1.0
vite: ^4.0.4
vite: ^4.1.4
dependencies:
'@popperjs/core': 2.11.6
'@remix-run/router': 1.3.2
'@sourcegraph/branded': link:../branded
'@sourcegraph/common': link:../common
'@sourcegraph/http-client': link:../http-client
@ -1172,17 +1174,17 @@ importers:
prismjs: 1.29.0
devDependencies:
'@playwright/test': 1.25.0
'@sveltejs/adapter-auto': 1.0.0-next.91_@sveltejs+kit@1.5.6
'@sveltejs/adapter-auto': 2.0.0_@sveltejs+kit@1.5.6
'@sveltejs/adapter-static': 2.0.1_@sveltejs+kit@1.5.6
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.1
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.4
'@types/cookie': 0.5.1
'@types/prismjs': 1.26.0
eslint-plugin-svelte3: 4.0.0_dbthnr4b2bdkhyiebwn7su3hnq
prettier-plugin-svelte: 2.9.0_jrsxveqmsx2uadbqiuq74wlc4u
svelte: 3.55.1
svelte-check: 3.0.3_svelte@3.55.1
svelte-check: 3.0.4_svelte@3.55.1
tslib: 2.1.0
vite: 4.1.1
vite: 4.1.4
client/wildcard:
specifiers:
@ -9913,12 +9915,12 @@ packages:
sucrase: 3.29.0
dev: true
/@sveltejs/adapter-auto/1.0.0-next.91_@sveltejs+kit@1.5.6:
resolution: {integrity: sha512-U57tQdzTfFINim8tzZSARC9ztWPzwOoHwNOpGdb2o6XrD0mEQwU9DsII7dBblvzg+xCnmd0pw7PDtXz5c5t96w==}
/@sveltejs/adapter-auto/2.0.0_@sveltejs+kit@1.5.6:
resolution: {integrity: sha512-b+gkHFZgD771kgV3aO4avHFd7y1zhmMYy9i6xOK7m/rwmwaRO8gnF5zBc0Rgca80B2PMU1bKNxyBTHA14OzUAQ==}
peerDependencies:
'@sveltejs/kit': ^1.0.0-next.587
'@sveltejs/kit': ^1.0.0
dependencies:
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.1
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.4
import-meta-resolve: 2.2.1
dev: true
@ -9927,10 +9929,10 @@ packages:
peerDependencies:
'@sveltejs/kit': ^1.5.0
dependencies:
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.1
'@sveltejs/kit': 1.5.6_svelte@3.55.1+vite@4.1.4
dev: true
/@sveltejs/kit/1.5.6_svelte@3.55.1+vite@4.1.1:
/@sveltejs/kit/1.5.6_svelte@3.55.1+vite@4.1.4:
resolution: {integrity: sha512-CHVeQpbcwSIUekF6WwevmExKXyfHnzM5nlCEUv13lGgfJ0zKA0ny5YwIe40vccrm5w5Caec7jMTt9Ih3McJr5g==}
engines: {node: ^16.14 || >=18}
hasBin: true
@ -9939,7 +9941,7 @@ packages:
svelte: ^3.54.0
vite: ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte': 2.0.2_svelte@3.55.1+vite@4.1.1
'@sveltejs/vite-plugin-svelte': 2.0.2_svelte@3.55.1+vite@4.1.4
'@types/cookie': 0.5.1
cookie: 0.5.0
devalue: 4.3.0
@ -9953,12 +9955,12 @@ packages:
svelte: 3.55.1
tiny-glob: 0.2.9
undici: 5.18.0
vite: 4.1.1
vite: 4.1.4
transitivePeerDependencies:
- supports-color
dev: true
/@sveltejs/vite-plugin-svelte/2.0.2_svelte@3.55.1+vite@4.1.1:
/@sveltejs/vite-plugin-svelte/2.0.2_svelte@3.55.1+vite@4.1.4:
resolution: {integrity: sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==}
engines: {node: ^14.18.0 || >= 16}
peerDependencies:
@ -9971,8 +9973,8 @@ packages:
magic-string: 0.27.0
svelte: 3.55.1
svelte-hmr: 0.15.1_svelte@3.55.1
vite: 4.1.1
vitefu: 0.2.4_vite@4.1.1
vite: 4.1.4
vitefu: 0.2.4_vite@4.1.4
transitivePeerDependencies:
- supports-color
dev: true
@ -31082,8 +31084,8 @@ packages:
react: 18.1.0
dev: false
/svelte-check/3.0.3_svelte@3.55.1:
resolution: {integrity: sha512-ByBFXo3bfHRGIsYEasHkdMhLkNleVfszX/Ns1oip58tPJlKdo5Ssr8kgVIuo5oq00hss8AIcdesuy0Xt0BcTvg==}
/svelte-check/3.0.4_svelte@3.55.1:
resolution: {integrity: sha512-feIyBAA5cSIxq4vq6mwGvGQTHy/wBVQbs5b+/VvE21WN8X7nonAuSqwvZv0UDBowzRka3Rh4gmLPH8rPePz3/w==}
hasBin: true
peerDependencies:
svelte: ^3.55.0
@ -33023,8 +33025,8 @@ packages:
replace-ext: 1.0.0
dev: true
/vite/4.1.1:
resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
/vite/4.1.4:
resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
@ -33056,7 +33058,7 @@ packages:
fsevents: 2.3.2
dev: true
/vitefu/0.2.4_vite@4.1.1:
/vitefu/0.2.4_vite@4.1.4:
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0
@ -33064,7 +33066,7 @@ packages:
vite:
optional: true
dependencies:
vite: 4.1.1
vite: 4.1.4
dev: true
/vlq/1.0.1: