sv-router 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Colin Lienard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # sv-router
2
+
3
+ https://www.npmjs.com/package/sv-router
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { Component } from 'svelte';
3
+ import RecursiveComponentTree from './RecursiveComponentTree.svelte';
4
+ import { paramsStore } from './router.svelte.ts';
5
+
6
+ let { tree }: { tree: Component[] } = $props();
7
+
8
+ const FirstComponent = $derived(tree[0]);
9
+ const restTree = $derived(tree.slice(1));
10
+ </script>
11
+
12
+ {#key restTree.length > 0 || Object.values(paramsStore)}
13
+ <FirstComponent>
14
+ {#if restTree.length > 0}
15
+ <RecursiveComponentTree tree={restTree}></RecursiveComponentTree>
16
+ {/if}
17
+ </FirstComponent>
18
+ {/key}
@@ -0,0 +1,8 @@
1
+ import type { Component } from 'svelte';
2
+ import RecursiveComponentTree from './RecursiveComponentTree.svelte';
3
+ type $$ComponentProps = {
4
+ tree: Component[];
5
+ };
6
+ declare const RecursiveComponentTree: Component<$$ComponentProps, {}, "">;
7
+ type RecursiveComponentTree = ReturnType<typeof RecursiveComponentTree>;
8
+ export default RecursiveComponentTree;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { on } from 'svelte/events';
3
+ import RecursiveComponentTree from './RecursiveComponentTree.svelte';
4
+ import { componentTree, onGlobalClick, onNavigate } from './router.svelte.ts';
5
+
6
+ onNavigate();
7
+
8
+ $effect(() => {
9
+ const off1 = on(globalThis, 'popstate', onNavigate);
10
+ const off2 = on(globalThis, 'click', onGlobalClick);
11
+
12
+ return () => {
13
+ off1();
14
+ off2();
15
+ };
16
+ });
17
+ </script>
18
+
19
+ <RecursiveComponentTree tree={componentTree}></RecursiveComponentTree>
@@ -0,0 +1,3 @@
1
+ import type { PathParams } from '../types/types.ts';
2
+ export type ConstructPathArgs<T extends string> = PathParams<T> extends never ? [T] : [T, PathParams<T>];
3
+ export declare function constructPath<T extends string>(...args: ConstructPathArgs<T>): string;
@@ -0,0 +1,10 @@
1
+ export function constructPath(...args) {
2
+ const [path, params] = args;
3
+ if (!params)
4
+ return path;
5
+ let result = path;
6
+ for (const key in params) {
7
+ result = result.replace(`:${key}`, params[key]);
8
+ }
9
+ return result;
10
+ }
@@ -0,0 +1,7 @@
1
+ import type { LayoutComponent, RouteComponent, Routes } from '../types/types.ts';
2
+ export declare function matchRoute(pathname: string, routes: Routes): {
3
+ match: RouteComponent | undefined;
4
+ layouts: LayoutComponent[];
5
+ params: Record<string, string>;
6
+ };
7
+ export declare function sortRoutes(routes: string[]): string[];
@@ -0,0 +1,65 @@
1
+ export function matchRoute(pathname, routes) {
2
+ // Remove trailing slash
3
+ if (pathname.length > 1 && pathname.endsWith('/')) {
4
+ pathname = pathname.slice(0, -1);
5
+ }
6
+ const pathParts = pathname.split('/');
7
+ const allRouteParts = sortRoutes(Object.keys(routes)).map((route) => route.split('/'));
8
+ let match;
9
+ const layouts = [];
10
+ let params = {};
11
+ outer: for (const routeParts of allRouteParts) {
12
+ for (const [index, routePart] of sortRoutes(routeParts).entries()) {
13
+ const pathPart = pathParts[index];
14
+ if (routePart.startsWith(':')) {
15
+ params[routePart.slice(1)] = pathPart;
16
+ }
17
+ else if (routePart === '*') {
18
+ match = routes[routeParts.join('/')];
19
+ break outer;
20
+ }
21
+ else if (routePart !== pathPart) {
22
+ break;
23
+ }
24
+ const routeMatch = routes[routeParts.join('/')];
25
+ if (typeof routeMatch !== 'function' &&
26
+ routeMatch?.layout &&
27
+ !layouts.includes(routeMatch.layout)) {
28
+ layouts.push(routeMatch.layout);
29
+ }
30
+ if (index === routeParts.length - 1) {
31
+ if (typeof routeMatch === 'function') {
32
+ if (routeParts.length === pathParts.length) {
33
+ match = routeMatch;
34
+ }
35
+ else {
36
+ continue;
37
+ }
38
+ }
39
+ else if (routeMatch) {
40
+ const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
41
+ const result = matchRoute(nestedPathname, routeMatch);
42
+ if (result) {
43
+ match = result.match;
44
+ params = { ...params, ...result.params };
45
+ layouts.push(...result.layouts);
46
+ }
47
+ }
48
+ break outer;
49
+ }
50
+ }
51
+ }
52
+ return { match, layouts, params };
53
+ }
54
+ export function sortRoutes(routes) {
55
+ return routes.toSorted((a, b) => getRoutePriority(a) - getRoutePriority(b));
56
+ }
57
+ function getRoutePriority(route) {
58
+ if (route === '' || route === '/')
59
+ return 1;
60
+ if (route === '*')
61
+ return 4;
62
+ if (route.includes(':'))
63
+ return 3;
64
+ return 2;
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
1
+ import { matchRoute, sortRoutes } from "./match-route.js";
2
+ const Home = (() => 'Home');
3
+ const Posts = (() => 'Posts');
4
+ const StaticPost = (() => 'StaticPost');
5
+ const DynamicPost = (() => 'DynamicPost');
6
+ const DynamicPostComment = (() => 'DynamicPostComment');
7
+ const UserNotFound = (() => 'UserNotFound');
8
+ const PageNotFound = (() => 'PageNotFound');
9
+ const Layout1 = (() => 'Layout1');
10
+ const Layout2 = (() => 'Layout2');
11
+ describe('matchRoute', () => {
12
+ describe.each([
13
+ {
14
+ mode: 'flat',
15
+ routes: {
16
+ '/': Home,
17
+ '/posts': Posts,
18
+ '/posts/static': StaticPost,
19
+ '/posts/:id': DynamicPost,
20
+ '/posts/:id/:commentId': DynamicPostComment,
21
+ '/users/*': UserNotFound,
22
+ '*': PageNotFound,
23
+ },
24
+ },
25
+ {
26
+ mode: 'flat unordered',
27
+ routes: {
28
+ '/posts/:id/:commentId': DynamicPostComment,
29
+ '/posts': Posts,
30
+ '/users/*': UserNotFound,
31
+ '/posts/static': StaticPost,
32
+ '*': PageNotFound,
33
+ '/posts/:id': DynamicPost,
34
+ '/': Home,
35
+ },
36
+ },
37
+ {
38
+ mode: 'tree',
39
+ routes: {
40
+ '/': Home,
41
+ '/posts': {
42
+ '/': Posts,
43
+ '/static': StaticPost,
44
+ '/:id': {
45
+ '/': DynamicPost,
46
+ '/:commentId': DynamicPostComment,
47
+ layout: Layout2,
48
+ },
49
+ layout: Layout1,
50
+ },
51
+ '/users/*': UserNotFound,
52
+ '*': PageNotFound,
53
+ },
54
+ },
55
+ {
56
+ mode: 'tree unordered',
57
+ routes: {
58
+ '*': PageNotFound,
59
+ '/posts': {
60
+ '/:id': {
61
+ '/:commentId': DynamicPostComment,
62
+ '/': DynamicPost,
63
+ layout: Layout2,
64
+ },
65
+ '/': Posts,
66
+ '/static': StaticPost,
67
+ layout: Layout1,
68
+ },
69
+ '/users/*': UserNotFound,
70
+ '/': Home,
71
+ },
72
+ },
73
+ ])('$mode paths', ({ mode, routes: r }) => {
74
+ const routes = r;
75
+ it('should match the root route', () => {
76
+ const { match } = matchRoute('/', routes);
77
+ expect(match).toEqual(Home);
78
+ });
79
+ it('should match a simple route', () => {
80
+ const { match } = matchRoute('/posts', routes);
81
+ expect(match).toEqual(Posts);
82
+ });
83
+ it('should match a simple route with a trailing slash', () => {
84
+ const { match } = matchRoute('/posts', routes);
85
+ expect(match).toEqual(Posts);
86
+ });
87
+ it('should match a nested route', () => {
88
+ const { match } = matchRoute('/posts/static', routes);
89
+ expect(match).toEqual(StaticPost);
90
+ });
91
+ it('should match a dynamic route and return a param', () => {
92
+ const { match, params } = matchRoute('/posts/bar', routes);
93
+ expect(match).toEqual(DynamicPost);
94
+ expect(params).toEqual({ id: 'bar' });
95
+ });
96
+ it('should match multiple dynamic nested routes and return params', () => {
97
+ const { match, params } = matchRoute('/posts/bar/baz', routes);
98
+ expect(match).toEqual(DynamicPostComment);
99
+ expect(params).toEqual({ id: 'bar', commentId: 'baz' });
100
+ });
101
+ if (mode === 'nested') {
102
+ it('should match routes with layout', () => {
103
+ const { layouts: layouts1 } = matchRoute('/', routes);
104
+ const { layouts: layouts2 } = matchRoute('/posts', routes);
105
+ const { layouts: layouts3 } = matchRoute('/posts/static', routes);
106
+ const { layouts: layouts4 } = matchRoute('/posts/bar/baz', routes);
107
+ expect(layouts1).toEqual([]);
108
+ expect(layouts2).toEqual([Layout1]);
109
+ expect(layouts3).toEqual([Layout1]);
110
+ expect(layouts4).toEqual([Layout1, Layout2]);
111
+ });
112
+ }
113
+ it('should match wildcard nested route', () => {
114
+ const { match } = matchRoute('/users/notfound', routes);
115
+ expect(match).toEqual(UserNotFound);
116
+ });
117
+ it('should match wildcard route', () => {
118
+ const { match } = matchRoute('/notfound', routes);
119
+ expect(match).toEqual(PageNotFound);
120
+ });
121
+ it('should not match any route', () => {
122
+ delete routes['*'];
123
+ const { match } = matchRoute('/notfound', routes);
124
+ expect(match).toBeUndefined();
125
+ });
126
+ });
127
+ });
128
+ describe('sortRoutes', () => {
129
+ it('should sort routes', () => {
130
+ const result = sortRoutes(['/:id', '*', '/foo', '', '/']);
131
+ expect(result).toEqual(['', '/', '/foo', '/:id', '*']);
132
+ });
133
+ });
@@ -0,0 +1,5 @@
1
+ import type { Component } from 'svelte';
2
+ import type { LazyRouteComponent, RouteComponent } from '../types/types.ts';
3
+ export declare function resolveRouteComponents(input: RouteComponent<any>[]): Promise<Component[]>;
4
+ export declare function resolveRouteComponent(input: RouteComponent): Promise<Component>;
5
+ export declare function isLazyImport(input: unknown): input is LazyRouteComponent;
@@ -0,0 +1,18 @@
1
+ export function resolveRouteComponents(input) {
2
+ return Promise.all(input.map((c) => resolveRouteComponent(c)));
3
+ }
4
+ export function resolveRouteComponent(input) {
5
+ return new Promise((resolve) => {
6
+ if (isLazyImport(input)) {
7
+ Promise.resolve(input()).then((module) => {
8
+ resolve(module.default);
9
+ });
10
+ }
11
+ else {
12
+ resolve(input);
13
+ }
14
+ });
15
+ }
16
+ export function isLazyImport(input) {
17
+ return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/g.test(String(input));
18
+ }
@@ -0,0 +1,3 @@
1
+ import type { Routes } from '../types/types.ts';
2
+ export declare function validateRoutes(routes: Routes): void;
3
+ export declare function getRoutePaths(routes: Routes): string[];
@@ -0,0 +1,34 @@
1
+ export function validateRoutes(routes) {
2
+ const paths = getRoutePaths(routes);
3
+ const wildcardPaths = paths.filter((path) => path.endsWith('*'));
4
+ for (const wildcardPath of wildcardPaths) {
5
+ const parentPath = wildcardPath.slice(0, -1);
6
+ const dynamicPath = paths.find((p) => p !== '/' &&
7
+ !p.endsWith('*') &&
8
+ p.startsWith(parentPath === '' ? '/:' : parentPath) &&
9
+ p.match(/:[^/]*$/g));
10
+ if (dynamicPath) {
11
+ console.warn(`Router warning: Wildcard route \`${wildcardPath}\` should not be at the same level as dynamic route \`${dynamicPath}\`.`);
12
+ }
13
+ }
14
+ }
15
+ export function getRoutePaths(routes) {
16
+ const paths = [];
17
+ for (const [key, value] of Object.entries(routes)) {
18
+ if (typeof value === 'object') {
19
+ paths.push(...getRoutePaths(value).map((path) => {
20
+ if (path === '*') {
21
+ return key + '/*';
22
+ }
23
+ if (path === '/') {
24
+ return key;
25
+ }
26
+ return key + path;
27
+ }));
28
+ }
29
+ else {
30
+ paths.push(key);
31
+ }
32
+ }
33
+ return paths;
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { getRoutePaths, validateRoutes } from "./validate-routes.js";
2
+ const component = (() => ({}));
3
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn());
4
+ beforeEach(() => {
5
+ vi.clearAllMocks();
6
+ });
7
+ describe('validateRoutes', () => {
8
+ it('should validate routes', () => {
9
+ validateRoutes({
10
+ '/': component,
11
+ '/about': component,
12
+ '/posts': {
13
+ '/': component,
14
+ '/:id': component,
15
+ },
16
+ '*': component,
17
+ });
18
+ expect(consoleSpy).not.toHaveBeenCalled();
19
+ });
20
+ it('should raise a warning if a wildcard route is at the same level as a dynamic route', () => {
21
+ validateRoutes({
22
+ '/': component,
23
+ '/:id': component,
24
+ '*': component,
25
+ });
26
+ expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `*` should not be at the same level as dynamic route `/:id`.');
27
+ });
28
+ it.each([
29
+ {
30
+ '/': component,
31
+ '/posts/*': component,
32
+ '/posts/:id': component,
33
+ '/:id': component,
34
+ '*': component,
35
+ },
36
+ {
37
+ '/': component,
38
+ '/posts': {
39
+ '*': component,
40
+ '/:id': component,
41
+ },
42
+ '/:id': component,
43
+ '*': component,
44
+ },
45
+ ])('should raise multiple warnings if wildcard routes are at the same level as dynamic routes', (routes) => {
46
+ validateRoutes(routes);
47
+ expect(consoleSpy).toHaveBeenCalledTimes(2);
48
+ expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `*` should not be at the same level as dynamic route `/:id`.');
49
+ expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `/posts/*` should not be at the same level as dynamic route `/posts/:id`.');
50
+ });
51
+ });
52
+ describe('getRoutePaths', () => {
53
+ it('should return all paths in an array', () => {
54
+ const result = getRoutePaths({
55
+ '/': component,
56
+ '/about': component,
57
+ '/posts': {
58
+ '/': component,
59
+ '/:id': component,
60
+ '*': component,
61
+ },
62
+ '/foo/bar': component,
63
+ '*': component,
64
+ });
65
+ expect(result).toEqual(['/', '/about', '/posts', '/posts/:id', '/posts/*', '/foo/bar', '*']);
66
+ });
67
+ });
@@ -0,0 +1,2 @@
1
+ export { default as Router } from './Router.svelte';
2
+ export { createRouter } from './router.svelte.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as Router } from './Router.svelte';
2
+ export { createRouter } from "./router.svelte.js";
@@ -0,0 +1,13 @@
1
+ import type { Component } from 'svelte';
2
+ import { type ConstructPathArgs } from './helpers/construct-path.ts';
3
+ import type { AllParams, Path, Routes } from './types/types.ts';
4
+ export declare let routes: Routes;
5
+ export declare const componentTree: Component<{}, {}, string>[];
6
+ export declare const paramsStore: Record<string, string>;
7
+ export declare function createRouter<T extends Routes>(r: T): {
8
+ path<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
9
+ goto<U extends Path<T>>(...args: ConstructPathArgs<U>): void;
10
+ params(): AllParams<T>;
11
+ };
12
+ export declare function onNavigate(): void;
13
+ export declare function onGlobalClick(event: Event): void;
@@ -0,0 +1,50 @@
1
+ import { BROWSER, DEV } from 'esm-env';
2
+ import { constructPath } from "./helpers/construct-path.js";
3
+ import { matchRoute } from "./helpers/match-route.js";
4
+ import { resolveRouteComponents } from "./helpers/utils.js";
5
+ export let routes;
6
+ export const componentTree = $state([]);
7
+ export const paramsStore = $state({});
8
+ export function createRouter(r) {
9
+ routes = r;
10
+ if (DEV && BROWSER) {
11
+ import('./helpers/validate-routes.ts').then(({ validateRoutes }) => {
12
+ validateRoutes(routes);
13
+ });
14
+ }
15
+ return {
16
+ path(...args) {
17
+ return constructPath(...args);
18
+ },
19
+ goto(...args) {
20
+ const path = constructPath(...args);
21
+ globalThis.history.pushState({}, '', path);
22
+ onNavigate();
23
+ },
24
+ params() {
25
+ const readonly = $derived(paramsStore);
26
+ return readonly;
27
+ },
28
+ };
29
+ }
30
+ export function onNavigate() {
31
+ const { match, layouts, params } = matchRoute(globalThis.location.pathname, routes);
32
+ resolveRouteComponents(match ? [...layouts, match] : layouts).then((components) => {
33
+ Object.assign(componentTree, components);
34
+ });
35
+ Object.assign(paramsStore, params);
36
+ }
37
+ export function onGlobalClick(event) {
38
+ const anchor = event.target.closest('a');
39
+ if (!anchor)
40
+ return;
41
+ if (anchor.hasAttribute('target') || anchor.hasAttribute('download'))
42
+ return;
43
+ const url = new URL(anchor.href);
44
+ const currentOrigin = globalThis.location.origin;
45
+ if (url.origin !== currentOrigin)
46
+ return;
47
+ event.preventDefault();
48
+ globalThis.history.pushState({}, '', anchor.href);
49
+ onNavigate();
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import type { Component, Snippet } from 'svelte';
2
+ type BaseProps = Record<string, any>;
3
+ export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
4
+ default: Component<Props>;
5
+ }>;
6
+ export type RouteComponent<Props extends BaseProps = BaseProps> = Component<Props> | LazyRouteComponent<Props>;
7
+ export type LayoutComponent = RouteComponent<{
8
+ children: Snippet;
9
+ }>;
10
+ export type Routes = {
11
+ [_: `/${string}`]: RouteComponent | Routes;
12
+ '*'?: RouteComponent;
13
+ layout?: LayoutComponent;
14
+ };
15
+ export type Path<T extends Routes> = RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>>>;
16
+ export type PathParams<T extends string> = ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
17
+ export type AllParams<T extends Routes> = Partial<Record<ExtractParams<RecursiveKeys<T>>, string>>;
18
+ type StripNonRoutes<T extends Routes> = {
19
+ [K in keyof T as K extends '*' ? never : K extends 'layout' ? never : K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
20
+ };
21
+ type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
22
+ [K in keyof T]: K extends string ? T[K] extends Routes ? RecursiveKeys<T[K], `${Prefix}${K}`> : `${Prefix}${K}` : never;
23
+ }[keyof T];
24
+ type RemoveLastSlash<T extends string> = T extends '/' ? T : T extends `${infer R}/` ? R : T;
25
+ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? Param : never;
26
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "sv-router",
3
+ "version": "0.0.1",
4
+ "description": "Modern Svelte routing",
5
+ "keywords": [
6
+ "svelte",
7
+ "router",
8
+ "spa"
9
+ ],
10
+ "homepage": "https://github.com/colinlienard/sv-router",
11
+ "bugs": {
12
+ "url": "https://github.com/colinlienard/sv-router/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/colinlienard/sv-router.git"
17
+ },
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "sv-router/source": "./src/index.ts",
23
+ "import": "./dist/index.js",
24
+ "svelte": "./dist/index.js",
25
+ "types": "./dist/index.d.ts"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "ex:basic": "pnpm --filter basic-example",
33
+ "build": "svelte-package -i src && rm -rf .svelte-kit",
34
+ "test": "vitest",
35
+ "check": "tsc --noEmit && pnpm -r check",
36
+ "lint": "eslint .",
37
+ "lint:fix": "eslint . --fix",
38
+ "format": "prettier . --check",
39
+ "format:fix": "prettier . --write",
40
+ "sherif": "sherif -r root-package-private-field",
41
+ "sherif:fix": "sherif -r root-package-private-field --fix"
42
+ },
43
+ "dependencies": {
44
+ "esm-env": "^1.1.4"
45
+ },
46
+ "devDependencies": {
47
+ "@eslint/js": "^9.15.0",
48
+ "@sveltejs/package": "^2.3.7",
49
+ "eslint-config-prettier": "^9.1.0",
50
+ "eslint-plugin-simple-import-sort": "^12.1.1",
51
+ "eslint-plugin-svelte": "^2.46.0",
52
+ "eslint-plugin-unicorn": "^56.0.1",
53
+ "globals": "^15.12.0",
54
+ "prettier": "^3.3.3",
55
+ "prettier-plugin-svelte": "^3.3.2",
56
+ "sherif": "^1.0.1",
57
+ "type-testing": "^0.2.0",
58
+ "typescript": "^5.7.2",
59
+ "typescript-eslint": "^8.14.0",
60
+ "vite": "^5.4.10",
61
+ "vitest": "^2.1.5"
62
+ },
63
+ "peerDependencies": {
64
+ "svelte": "^5"
65
+ },
66
+ "packageManager": "pnpm@9.14.2"
67
+ }