sv-router 0.15.0 → 0.16.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/README.md CHANGED
@@ -5,10 +5,11 @@
5
5
  # sv-router
6
6
 
7
7
  [![npm](https://badgen.net/npm/v/sv-router)](https://www.npmjs.com/package/sv-router)
8
- [![install size](https://packagephobia.com/badge?p=sv-router)](https://packagephobia.com/result?p=sv-router)
8
+ ![downloads](https://badgen.net/npm/dm/sv-router)
9
+ ![size](https://badgen.net/bundlephobia/minzip/sv-router)
9
10
  [![codecov](https://codecov.io/github/colinlienard/sv-router/graph/badge.svg?token=C9RBSEFO9S)](https://codecov.io/github/colinlienard/sv-router)
10
11
 
11
- A feature-rich yet intuitive routing library for Svelte single-page apps.
12
+ Flexible, ergonomic, and complete Svelte 5 router.
12
13
 
13
14
  [Documentation](https://sv-router.vercel.app/) • [Getting Started](https://sv-router.vercel.app/guide/getting-started) • [Reference](https://sv-router.vercel.app/reference)
14
15
 
@@ -18,16 +19,14 @@ A feature-rich yet intuitive routing library for Svelte single-page apps.
18
19
 
19
20
  ## Features
20
21
 
21
- - 🔒 **Typesafe navigation**: Get autocomplete and type checking for your routes.
22
- - 🔄 **Flexibility**: Choose between code-based or file-based routing approaches.
23
- - 🌿 **Nested routes**: Create complex layouts with ease.
22
+ - 🔒 **Type-safe navigation**: Catch broken links before you ship.
23
+ - 🗂️ **File-based routing (optional)**: DX of a meta-framework-like approach.
24
+ - 🪶 **Lightweight**: < 5kB gzipped.
25
+ - ⚡ **Performance**: Built-in code splitting and preloading.
24
26
  - 🔍 **Reactive search params**: For simpler state management in the URL.
27
+ - 🌿 **Nested routes**: Share layouts across pages.
25
28
  - 🛡️ **Hooks**: For navigation guards, data loading, or analytics tracking.
26
- - **Performance**: Optimized for speed with built-in code splitting and preloading.
27
- - 🧩 **Familiar API**: Follows established conventions from popular meta frameworks.
28
- - 🪶 **Lightweight**: Minimal impact on your bundle size.
29
- - 🚀 **Made for Svelte 5**: True Svelte 5 implementation with the latest features.
30
- - #️⃣ **Hash-based routing**: Hash-based routing enables usage in local environments.
29
+ - #️⃣ **Hash-based routing**: Works inside Electron/Tauri.
31
30
 
32
31
  ## Getting Started
33
32
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.15.0",
4
- "description": "Modern Svelte Routing",
3
+ "version": "0.16.1",
4
+ "description": "Type-safe routing for Svelte SPAs",
5
5
  "keywords": [
6
6
  "svelte",
7
7
  "router",
@@ -28,7 +28,6 @@
28
28
  "types": "./src/vite-plugin/index.d.ts"
29
29
  }
30
30
  },
31
- "bin": "./src/cli/index.js",
32
31
  "files": [
33
32
  "src"
34
33
  ],
@@ -36,30 +35,30 @@
36
35
  "esm-env": "^1.2.2"
37
36
  },
38
37
  "devDependencies": {
39
- "@changesets/cli": "^2.30.0",
38
+ "@changesets/cli": "^2.31.0",
40
39
  "@eslint/js": "^10.0.1",
41
- "@sveltejs/vite-plugin-svelte": "^7.0.0",
40
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
42
41
  "@testing-library/jest-dom": "^6.9.1",
43
42
  "@testing-library/svelte": "^5.3.1",
44
43
  "@testing-library/user-event": "^14.6.1",
45
- "@types/node": "^24.12.0",
46
- "@vitest/coverage-v8": "^4.1.0",
47
- "eslint": "^10.0.3",
44
+ "@types/node": "^24.12.3",
45
+ "@vitest/coverage-v8": "^4.1.5",
46
+ "eslint": "^10.3.0",
48
47
  "eslint-config-prettier": "^10.1.8",
49
- "eslint-plugin-simple-import-sort": "^12.1.1",
50
- "eslint-plugin-svelte": "^3.15.2",
48
+ "eslint-plugin-simple-import-sort": "^13.0.0",
49
+ "eslint-plugin-svelte": "^3.17.1",
51
50
  "eslint-plugin-unicorn": "^64.0.0",
52
- "globals": "^17.4.0",
53
- "happy-dom": "^20.8.4",
54
- "prettier": "^3.8.1",
51
+ "globals": "^17.6.0",
52
+ "happy-dom": "^20.9.0",
53
+ "prettier": "^3.8.3",
55
54
  "prettier-plugin-jsdoc": "^1.8.0",
56
55
  "prettier-plugin-svelte": "^3.5.1",
57
- "svelte-check": "^4.4.5",
56
+ "svelte-check": "^4.4.8",
58
57
  "type-testing": "^0.2.0",
59
- "typescript": "^6.0.2",
60
- "typescript-eslint": "^8.57.1",
61
- "vite": "^8.0.0",
62
- "vitest": "^4.1.0"
58
+ "typescript": "^6.0.3",
59
+ "typescript-eslint": "^8.59.2",
60
+ "vite": "^8.0.11",
61
+ "vitest": "^4.1.5"
63
62
  },
64
63
  "peerDependencies": {
65
64
  "svelte": "^5"
@@ -78,5 +77,8 @@
78
77
  "format": "prettier . --check",
79
78
  "format:fix": "prettier . --write",
80
79
  "changeset": "changeset"
80
+ },
81
+ "bin": {
82
+ "sv-router": "./src/cli/index.js"
81
83
  }
82
84
  }
package/src/Router.svelte CHANGED
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { untrack } from 'svelte';
2
3
  import { on } from 'svelte/events';
3
4
  import {
4
5
  componentTree,
@@ -7,14 +8,19 @@
7
8
  onGlobalClick,
8
9
  onNavigate,
9
10
  } from './create-router.svelte.js';
11
+ import { getUserState } from './helpers/utils.js';
10
12
  import RecursiveComponentTree from './RecursiveComponentTree.svelte';
11
13
 
12
14
  /** @type {{ base?: string }} */
13
15
  let { base: basename } = $props();
14
16
 
15
- init(basename);
17
+ init(untrack(() => basename));
16
18
 
17
- onNavigate();
19
+ onNavigate(undefined, {
20
+ search: location.search,
21
+ hash: location.hash,
22
+ state: getUserState(history.state),
23
+ });
18
24
 
19
25
  $effect(() => {
20
26
  const cleanup = [
@@ -23,25 +23,3 @@ export function isActiveLink({ className = 'is-active', startsWith = false } = {
23
23
  });
24
24
  };
25
25
  }
26
-
27
- /** @type {import('./index.d.ts').IsActiveLinkAction} */
28
- export function isActiveLinkAction(node, { className = 'is-active', startsWith = false } = {}) {
29
- if (node.tagName !== 'A') {
30
- throw new Error('isActiveLink can only be used on <a> elements');
31
- }
32
-
33
- $effect(() => {
34
- let pathname;
35
- if (base.name === '#') {
36
- pathname = new URL(node.href).hash.slice(1);
37
- } else {
38
- pathname = new URL(node.href).pathname;
39
- }
40
- const tokens = className.split(' ').filter(Boolean) ?? [];
41
- if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
42
- node.classList.add(...tokens);
43
- } else {
44
- node.classList.remove(...tokens);
45
- }
46
- });
47
- }
@@ -165,8 +165,14 @@ function mergeRouteGroup(result, childMap) {
165
165
  } else if (!Array.isArray(val)) {
166
166
  routeWithGroupFiles = { ...val };
167
167
  }
168
- if (layout) routeWithGroupFiles.layout = /** @type {string} */ (layout);
169
- if (hooks) routeWithGroupFiles.hooks = /** @type {string} */ (hooks);
168
+ if (layout) {
169
+ if (routeWithGroupFiles.layout) {
170
+ routeWithGroupFiles = { '/': routeWithGroupFiles, layout: layout };
171
+ } else {
172
+ routeWithGroupFiles.layout = layout;
173
+ }
174
+ }
175
+ if (hooks) routeWithGroupFiles.hooks = hooks;
170
176
  if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
171
177
  if (result[key]) {
172
178
  throw new Error(`Route conflict at \`${key}\``);
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-console */
2
2
  import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { genConfig } from './config.js';
4
5
  import { generateRouterCode } from './generate-router-code.js';
5
6
 
@@ -10,14 +11,18 @@ export function writeRouterCode() {
10
11
  }
11
12
 
12
13
  // Write `.router/tsconfig.json` file
14
+ const tsMajor = getTypeScriptMajorVersion();
15
+ const useBaseUrl = tsMajor === undefined || tsMajor < 6;
16
+ let alias = genConfig.routerPath;
17
+ if (!useBaseUrl) {
18
+ alias = alias.replace(genConfig.genCodeDirPath, '.');
19
+ }
13
20
  const tsConfig = {
14
21
  compilerOptions: {
15
22
  module: 'preserve',
16
23
  moduleResolution: 'bundler',
17
- baseUrl: '..',
18
- paths: {
19
- [genConfig.genCodeAlias]: [genConfig.routerPath],
20
- },
24
+ ...(useBaseUrl ? { baseUrl: '..' } : {}),
25
+ paths: { [genConfig.genCodeAlias]: [alias] },
21
26
  },
22
27
  include: [
23
28
  '../src/**/*.js',
@@ -62,3 +67,14 @@ function writeFileIfDifferent(filePath, content) {
62
67
  return true;
63
68
  }
64
69
  }
70
+
71
+ function getTypeScriptMajorVersion() {
72
+ try {
73
+ const require = createRequire(process.cwd() + '/package.json');
74
+ const tsPackagePath = require.resolve('typescript/package.json');
75
+ const tsPackage = JSON.parse(fs.readFileSync(tsPackagePath, 'utf8'));
76
+ return Number(tsPackage.version.split('.')[0]);
77
+ } catch {
78
+ return;
79
+ }
80
+ }
@@ -104,7 +104,7 @@ export function stripBase(pathname) {
104
104
  }
105
105
 
106
106
  /** @param {any} state */
107
- function getUserState(state) {
107
+ export function getUserState(state) {
108
108
  if (state && '_userState' in state) {
109
109
  return state._userState;
110
110
  }
package/src/index.d.ts CHANGED
@@ -2,19 +2,6 @@ import type { Component, Snippet } from 'svelte';
2
2
  import type { Action } from 'svelte/action';
3
3
  import type { Attachment } from 'svelte/attachments';
4
4
 
5
- /**
6
- * @deprecated Use the `isActiveLink` [attachment](https://svelte.dev/docs/svelte/@attach) instead.
7
- *
8
- * A Svelte action that will add a class to the anchor if its `href` matches the current route. It
9
- * can have an optional `className` parameter to specify the class to add, otherwise it will
10
- * default to `is-active`, and an optional `startsWith` parameter.
11
- *
12
- * ```svelte
13
- * <a href={p('/about')} use:isActiveLink={{ className: 'active-link' }}>
14
- * ```
15
- */
16
- export const isActiveLinkAction: IsActiveLinkAction;
17
-
18
5
  /**
19
6
  * A Svelte attachment that will add a class to the anchor if its `href` matches the current route.
20
7
  * It can have an optional `className` parameter to specify the class to add, otherwise it will
@@ -329,6 +316,7 @@ export type SearchParams = Omit<
329
316
  getAll(name: string): (string | number | boolean)[];
330
317
  set(name: string, value: string | number | boolean, options?: { replace?: boolean }): void;
331
318
  sort(options?: { replace?: boolean }): void;
319
+ toURLSearchParams(): URLSearchParams;
332
320
  values(): (string | number | boolean)[];
333
321
  };
334
322
 
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { isActiveLink, isActiveLinkAction } from './attachments.svelte.js';
1
+ export { isActiveLink } from './attachments.svelte.js';
2
2
  export { blockNavigation, createRouter } from './create-router.svelte.js';
3
3
  export { serializeSearch } from './helpers/utils.js';
4
4
  export { Navigation } from './navigation.js';
@@ -47,6 +47,9 @@ const shell = {
47
47
  values() {
48
48
  return [...searchParams.values()].map(parseSearchValue);
49
49
  },
50
+ toURLSearchParams() {
51
+ return new URLSearchParams(searchParams);
52
+ },
50
53
  get size() {
51
54
  return searchParams.size;
52
55
  },
@@ -66,7 +69,8 @@ export function syncSearchParams(search) {
66
69
  for (const [key, value] of newSearch) {
67
70
  searchParams.set(key, value);
68
71
  }
69
- for (const key of searchParams.keys()) {
72
+ // eslint-disable-next-line unicorn/no-useless-spread
73
+ for (const key of [...searchParams.keys()]) {
70
74
  if (!newSearch.has(key)) {
71
75
  searchParams.delete(key);
72
76
  }