sv-router 0.0.8 → 0.2.0

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/README.md CHANGED
@@ -18,11 +18,14 @@ A feature-rich yet intuitive routing library for Svelte single-page apps.
18
18
  ## Features
19
19
 
20
20
  - 🔒 **Typesafe navigation**: Get autocomplete and type checking for your routes.
21
- - 🗂ïļ **File-based routing (optional)**: Enjoy the DX of a meta-framework-like approach.
21
+ - 🔄 **Flexibility**: Choose between code-based or file-based routing approaches.
22
22
  - ðŸŒŋ **Nested routes**: Create complex layouts with ease.
23
+ - 🔍 **Reactive search params**: For simpler state management in the URL.
24
+ - ðŸ›Ąïļ **Hooks**: For navigation guards, data loading, or analytics tracking.
23
25
  - ⚡ **Performance**: Optimized for speed with built-in code splitting and preloading.
24
- - ðŸ§Đ **Familiar API**: Follows established conventions from popular meta frameworks
25
- - 🚀 **Made for Svelte 5**: Benefit from faster performance and smaller bundle size.
26
+ - ðŸ§Đ **Familiar API**: Follows established conventions from popular meta frameworks.
27
+ - ðŸŠķ **Lightweight**: Minimal impact on your bundle size.
28
+ - 🚀 **Made for Svelte 5**: True Svelte 5 implementation with the latest features.
26
29
 
27
30
  ## Installation
28
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.0.8",
3
+ "version": "0.2.0",
4
4
  "description": "Modern Svelte Routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -37,23 +37,23 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@changesets/cli": "^2.28.1",
40
- "@eslint/js": "^9.21.0",
41
- "@types/node": "^22.13.8",
42
- "eslint": "^9.21.0",
43
- "eslint-config-prettier": "^10.0.2",
40
+ "@eslint/js": "^9.23.0",
41
+ "@types/node": "^22.13.14",
42
+ "eslint": "^9.23.0",
43
+ "eslint-config-prettier": "^10.1.1",
44
44
  "eslint-plugin-simple-import-sort": "^12.1.1",
45
- "eslint-plugin-svelte": "^3.0.2",
46
- "eslint-plugin-unicorn": "^57.0.0",
45
+ "eslint-plugin-svelte": "^3.3.3",
46
+ "eslint-plugin-unicorn": "^58.0.0",
47
47
  "globals": "^16.0.0",
48
- "prettier": "^3.5.2",
48
+ "prettier": "^3.5.3",
49
49
  "prettier-plugin-jsdoc": "^1.3.2",
50
50
  "prettier-plugin-svelte": "^3.3.3",
51
- "svelte-check": "^4.1.4",
51
+ "svelte-check": "^4.1.5",
52
52
  "type-testing": "^0.2.0",
53
53
  "typescript": "^5.8.2",
54
- "typescript-eslint": "^8.25.0",
55
- "vite": "^6.2.0",
56
- "vitest": "^3.0.7"
54
+ "typescript-eslint": "^8.28.0",
55
+ "vite": "^6.2.3",
56
+ "vitest": "^3.0.9"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "svelte": "^5"
@@ -64,7 +64,7 @@
64
64
  "docs:preview": "pnpm --filter docs preview",
65
65
  "test": "vitest",
66
66
  "check": "svelte-check && pnpm -r check",
67
- "lint": "eslint .",
67
+ "lint": "eslint . --max-warnings 0",
68
68
  "lint:fix": "eslint . --fix",
69
69
  "format": "prettier . --check",
70
70
  "format:fix": "prettier . --write",
package/src/Router.svelte CHANGED
@@ -1,8 +1,21 @@
1
1
  <script>
2
2
  import { on } from 'svelte/events';
3
- import { componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
3
+ import { base, componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
4
+ import { join } from './helpers/utils.js';
4
5
  import RecursiveComponentTree from './RecursiveComponentTree.svelte';
5
6
 
7
+ /** @type {{ base?: string }} */
8
+ let { base: basename } = $props();
9
+
10
+ if (basename) {
11
+ base.name = (basename.startsWith('/') ? '' : '/') + basename;
12
+ const url = new URL(globalThis.location.href);
13
+ if (!url.pathname.startsWith(base.name)) {
14
+ url.pathname = join(base.name, url.pathname);
15
+ history.replaceState(history.state || {}, '', url.href);
16
+ }
17
+ }
18
+
6
19
  onNavigate();
7
20
 
8
21
  $effect(() => {
@@ -2,11 +2,11 @@ import { BROWSER, DEV } from 'esm-env';
2
2
  import { isActive } from './helpers/is-active.js';
3
3
  import { matchRoute } from './helpers/match-route.js';
4
4
  import { preloadOnHover } from './helpers/preload-on-hover.js';
5
- import { constructPath, resolveRouteComponents } from './helpers/utils.js';
5
+ import { constructPath, join, resolveRouteComponents } from './helpers/utils.js';
6
6
  import { syncSearchParams } from './search-params.svelte.js';
7
7
 
8
8
  /** @type {import('./index.d.ts').Routes} */
9
- export let routes;
9
+ let routes;
10
10
 
11
11
  /** @type {{ value: import('svelte').Component[] }} */
12
12
  export let componentTree = $state({ value: [] });
@@ -16,6 +16,11 @@ export let params = $state({ value: {} });
16
16
 
17
17
  export let location = $state(updatedLocation());
18
18
 
19
+ /** @type {{ name?: string }} */
20
+ export const base = {
21
+ name: undefined,
22
+ };
23
+
19
24
  /**
20
25
  * @template {import('./index.d.ts').Routes} T
21
26
  * @param {T} r
@@ -85,12 +90,11 @@ export async function onNavigate(path, options = {}) {
85
90
  if (!routes) {
86
91
  throw new Error('Router not initialized: `createRouter` was not called.');
87
92
  }
88
- const {
89
- match,
90
- layouts,
91
- hooks,
92
- params: newParams,
93
- } = matchRoute(path || globalThis.location.pathname, routes);
93
+ let matchPath = path || globalThis.location.pathname;
94
+ if (base.name && matchPath.startsWith(base.name)) {
95
+ matchPath = matchPath.slice(base.name.length) || '/';
96
+ }
97
+ const { match, layouts, hooks, params: newParams } = matchRoute(matchPath, routes);
94
98
 
95
99
  for (const { beforeLoad } of hooks) {
96
100
  try {
@@ -107,12 +111,17 @@ export async function onNavigate(path, options = {}) {
107
111
  if (options.search) path += options.search;
108
112
  if (options.hash) path += options.hash;
109
113
  const historyMethod = options.replace ? 'replaceState' : 'pushState';
110
- globalThis.history[historyMethod](options.state || {}, '', path);
114
+ const to = base.name ? join(base.name, path) : path;
115
+ globalThis.history[historyMethod](options.state || {}, '', to);
111
116
  }
112
117
 
113
118
  syncSearchParams();
114
119
  Object.assign(location, updatedLocation());
115
120
 
121
+ if (options.scrollToTop !== false) {
122
+ window.scrollTo({ top: 0, left: 0, behavior: options.scrollToTop });
123
+ }
124
+
116
125
  for (const { afterLoad } of hooks) {
117
126
  afterLoad?.();
118
127
  }
@@ -130,12 +139,13 @@ export function onGlobalClick(event) {
130
139
  if (url.origin !== currentOrigin) return;
131
140
 
132
141
  event.preventDefault();
133
- const { replace, state } = anchor.dataset;
142
+ const { replace, state, scrollToTop } = anchor.dataset;
134
143
  onNavigate(url.pathname, {
135
144
  replace: replace === '' || replace === 'true',
136
145
  search: url.search,
137
146
  state,
138
147
  hash: url.hash,
148
+ scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
139
149
  });
140
150
  }
141
151
 
@@ -143,7 +153,7 @@ function updatedLocation() {
143
153
  return {
144
154
  pathname: globalThis.location.pathname,
145
155
  search: globalThis.location.search,
146
- state: globalThis.history.state,
156
+ state: history.state,
147
157
  hash: globalThis.location.hash,
148
158
  };
149
159
  }
@@ -9,11 +9,11 @@ import path from 'node:path';
9
9
  * }} GeneratedRoutes
10
10
  */
11
11
 
12
- const FILENAME_REGEX = /\(?([\w-]+)\)?(\.lazy)?\.svelte$/; // any.svelte, any.lazy.svelte, (any).svelte
13
- const PARAM_FILENAME_REGEX = /\(?\[(.*)\]\)?(\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte, ([any]).svelte
14
- const CATCH_ALL_FILENAME_REGEX = /\(?\[\.\.\.(.*)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
15
- const OUT_OF_LAYOUT_FILENAME_REGEX = /\(\[\.?\.?\.?(.*)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
16
- const HOOKS_FILENAME_REGEX = /(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
12
+ const FILENAME_REGEX = /(?<=[/.]|^)\(?([\w-]+)\)?(\.lazy)?\.svelte$/; // any.svelte, any.lazy.svelte, (any).svelte
13
+ const PARAM_FILENAME_REGEX = /(?<=[/.]|^)\(?\[([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte, ([any]).svelte
14
+ const CATCH_ALL_FILENAME_REGEX = /(?<=[/.]|^)\(?\[\.\.\.([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
15
+ const OUT_OF_LAYOUT_FILENAME_REGEX = /(?<=[/.]|^)\(\[\.?\.?\.?([\w-]+)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
16
+ const HOOKS_FILENAME_REGEX = /(?<=[/.]|^)(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
17
17
 
18
18
  /**
19
19
  * @param {string} routesPath
@@ -9,10 +9,6 @@ export function writeRouterCode() {
9
9
  fs.mkdirSync(genConfig.genCodeDirPath);
10
10
  }
11
11
 
12
- // Write `.router/router.ts` file
13
- const routerCode = generateRouterCode(genConfig.routesPath);
14
- writeFileIfDifferent(genConfig.routerPath, routerCode);
15
-
16
12
  // Write `.router/tsconfig.json` file
17
13
  const tsConfig = {
18
14
  compilerOptions: {
@@ -35,7 +31,15 @@ export function writeRouterCode() {
35
31
  };
36
32
  writeFileIfDifferent(genConfig.tsconfigPath, JSON.stringify(tsConfig, undefined, 2));
37
33
 
38
- console.log('✅ïļ Routes generated');
34
+ // Write `.router/router.ts` file
35
+ const routerCode = generateRouterCode(genConfig.routesPath);
36
+ const written = writeFileIfDifferent(genConfig.routerPath, routerCode);
37
+
38
+ if (written) {
39
+ console.log('✅ïļ Routes generated');
40
+ } else {
41
+ console.log('✅ïļ Routes already up to date');
42
+ }
39
43
  } catch (error) {
40
44
  console.error(
41
45
  'Error during routes generation:',
@@ -51,5 +55,6 @@ export function writeRouterCode() {
51
55
  function writeFileIfDifferent(filePath, content) {
52
56
  if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
53
57
  fs.writeFileSync(filePath, content);
58
+ return true;
54
59
  }
55
60
  }
@@ -44,3 +44,18 @@ export function resolveRouteComponent(input) {
44
44
  export function isLazyImport(input) {
45
45
  return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/.test(String(input));
46
46
  }
47
+
48
+ /** @param {...string} parts */
49
+ export function join(...parts) {
50
+ let result = '';
51
+ for (let part of parts) {
52
+ if (!part.startsWith('/')) {
53
+ result += '/';
54
+ }
55
+ if (part.endsWith('/')) {
56
+ part = part.slice(0, -1);
57
+ }
58
+ result += part;
59
+ }
60
+ return result;
61
+ }
package/src/index.d.ts CHANGED
@@ -23,13 +23,16 @@ export const isActiveLink: IsActiveLink;
23
23
  * ```
24
24
  */
25
25
  export function createRouter<T extends Routes>(r: T): RouterApi<T>;
26
- /** The component that will render the current route. */
27
- export const Router: Component;
26
+ /**
27
+ * The component that will render the current route. You can pass a `base` prop to set the base path
28
+ * that is prepended to every url.
29
+ */
30
+ export const Router: Component<{ base?: string }>;
28
31
  /**
29
32
  * The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
30
33
  * will update the url on change.
31
34
  */
32
- export const searchParams: URLSearchParams;
35
+ export const searchParams: SearchParams;
33
36
 
34
37
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35
38
  type BaseProps = {};
@@ -162,9 +165,17 @@ export type NavigateOptions =
162
165
  search?: string;
163
166
  state?: string;
164
167
  hash?: string;
168
+ scrollToTop?: ScrollBehavior | false;
165
169
  }
166
170
  | undefined;
167
171
 
172
+ export type SearchParams = URLSearchParams & {
173
+ append: (name: string, value: string, options?: { replace?: boolean }) => void;
174
+ delete: (name: string, value?: string, options?: { replace?: boolean }) => void;
175
+ set: (name: string, value: string, options?: { replace?: boolean }) => void;
176
+ sort: (options?: { replace?: boolean }) => void;
177
+ };
178
+
168
179
  type NavigateArgs<T extends string> =
169
180
  | (PathParams<T> extends never
170
181
  ? [T] | [T, NavigateOptions]
@@ -2,15 +2,15 @@ import { SvelteURLSearchParams } from 'svelte/reactivity';
2
2
 
3
3
  let searchParams = new SvelteURLSearchParams(globalThis.location.search);
4
4
 
5
- /** @type {URLSearchParams} */
5
+ /** @type {import('./index.js').SearchParams} */
6
6
  const shell = {
7
- append(...args) {
8
- searchParams.append(...args);
9
- updateUrlSearchParams();
7
+ append(name, value, options) {
8
+ searchParams.append(name, value);
9
+ updateUrlSearchParams(options);
10
10
  },
11
- delete(...args) {
12
- searchParams.delete(...args);
13
- updateUrlSearchParams();
11
+ delete(name, value, options) {
12
+ searchParams.delete(name, value);
13
+ updateUrlSearchParams(options);
14
14
  },
15
15
  entries() {
16
16
  return searchParams.entries();
@@ -31,13 +31,13 @@ const shell = {
31
31
  keys() {
32
32
  return searchParams.keys();
33
33
  },
34
- set(...args) {
35
- searchParams.set(...args);
36
- updateUrlSearchParams();
34
+ set(name, value, options) {
35
+ searchParams.set(name, value);
36
+ updateUrlSearchParams(options);
37
37
  },
38
- sort() {
38
+ sort(options) {
39
39
  searchParams.sort();
40
- updateUrlSearchParams();
40
+ updateUrlSearchParams(options);
41
41
  },
42
42
  toString() {
43
43
  return searchParams.toString();
@@ -60,19 +60,17 @@ export function syncSearchParams() {
60
60
  if (searchParams.toString() === newSearchParams.toString()) {
61
61
  return;
62
62
  }
63
-
64
- for (const key of searchParams.keys()) {
65
- searchParams.delete(key);
66
- }
63
+ searchParams = new SvelteURLSearchParams();
67
64
  for (const [key, value] of newSearchParams.entries()) {
68
65
  searchParams.append(key, value);
69
66
  }
70
67
  }
71
68
 
72
- function updateUrlSearchParams() {
69
+ /** @param {{ replace?: boolean }} [options] */
70
+ function updateUrlSearchParams(options) {
73
71
  let url = globalThis.location.origin + globalThis.location.pathname;
74
72
  if (searchParams.size > 0) {
75
73
  url += '?' + searchParams.toString();
76
74
  }
77
- globalThis.history.pushState({}, '', url);
75
+ globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
78
76
  }