sv-router 0.0.7 → 0.0.8

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
@@ -1,3 +1,37 @@
1
+ <div align="center">
2
+
3
+ <img src="./docs/public/logo.svg" alt="" height="128px">
4
+
1
5
  # sv-router
2
6
 
3
- https://www.npmjs.com/package/sv-router
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)
9
+
10
+ A feature-rich yet intuitive routing library for Svelte single-page apps.
11
+
12
+ [Documentation](https://sv-router.vercel.app/) • [Getting Started](https://sv-router.vercel.app/guide/getting-started) • [Reference](https://sv-router.vercel.app/reference)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## Features
19
+
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.
22
+ - 🌿 **Nested routes**: Create complex layouts with ease.
23
+ - ⚡ **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
+
27
+ ## Installation
28
+
29
+ Add it to an existing Svelte project:
30
+
31
+ ```bash
32
+ npm install sv-router
33
+ ```
34
+
35
+ ## License
36
+
37
+ [MIT](./LICENSE) © Colin Lienard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Modern Svelte Routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -1,14 +1,14 @@
1
1
  import { location } from './create-router.svelte.js';
2
2
 
3
3
  /** @type {import('./index.d.ts').IsActiveLink} */
4
- export function isActiveLink(node, { className = 'is-active' } = {}) {
4
+ export function isActiveLink(node, { className = 'is-active', startsWith = false } = {}) {
5
5
  if (node.tagName !== 'A') {
6
6
  throw new Error('isActiveLink can only be used on <a> elements');
7
7
  }
8
8
 
9
9
  $effect(() => {
10
10
  const pathname = new URL(node.href).pathname;
11
- if (pathname === location.pathname) {
11
+ if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
12
12
  node.classList.add(className);
13
13
  } else {
14
14
  node.classList.remove(className);
@@ -9,8 +9,10 @@ import path from 'node:path';
9
9
  * }} GeneratedRoutes
10
10
  */
11
11
 
12
- const PARAM_FILENAME_REGEX = /\[(.*)\](\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte
13
- const CATCH_ALL_FILENAME_REGEX = /\[\.\.\.(.*)\](\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte
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
14
16
  const HOOKS_FILENAME_REGEX = /(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
15
17
 
16
18
  /**
@@ -18,9 +20,13 @@ const HOOKS_FILENAME_REGEX = /(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.
18
20
  * @returns {string}
19
21
  */
20
22
  export function generateRouterCode(routesPath) {
21
- const fileTree = buildFileTree(path.join(process.cwd(), routesPath));
23
+ const absoluteRoutesPath = path.join(process.cwd(), routesPath);
24
+ if (!fs.existsSync(absoluteRoutesPath)) {
25
+ throw new Error(`Routes directory not found at \`${routesPath}\``);
26
+ }
27
+ const fileTree = buildFileTree(absoluteRoutesPath);
22
28
  const routeMap = createRouteMap(fileTree);
23
- return createRouterCode(routeMap, path.join('..', routesPath));
29
+ return createRouterCode(routeMap, path.posix.join('..', routesPath));
24
30
  }
25
31
 
26
32
  /**
@@ -74,15 +80,20 @@ export function createRouteMap(fileTree, prefix = '') {
74
80
  continue;
75
81
  }
76
82
 
77
- const catchAll = CATCH_ALL_FILENAME_REGEX.exec(entry);
78
- if (catchAll) {
79
- result['*' + catchAll[1]] = prefix + entry;
83
+ if (CATCH_ALL_FILENAME_REGEX.test(entry)) {
84
+ const replacement = OUT_OF_LAYOUT_FILENAME_REGEX.test(entry) ? '(*$1)' : '*$1';
85
+ let key = filePathToRoute(entry.replace(CATCH_ALL_FILENAME_REGEX, replacement));
86
+ if (!key.startsWith('*') && !key.startsWith('(*')) {
87
+ key = '/' + key;
88
+ }
89
+ result[key] = prefix + entry;
80
90
  continue;
81
91
  }
82
92
 
83
- // Match [id].svelte
84
93
  if (PARAM_FILENAME_REGEX.test(entry)) {
85
- result['/' + filePathToRoute(entry.replace(PARAM_FILENAME_REGEX, ':$1'))] = prefix + entry;
94
+ const replacement = OUT_OF_LAYOUT_FILENAME_REGEX.test(entry) ? '(:$1)' : ':$1';
95
+ const key = '/' + filePathToRoute(entry.replace(PARAM_FILENAME_REGEX, replacement));
96
+ result[key] = prefix + entry;
86
97
  continue;
87
98
  }
88
99
 
@@ -176,7 +187,7 @@ export function pathToCorrectCasing(value) {
176
187
  extractLastPart(CATCH_ALL_FILENAME_REGEX) ||
177
188
  extractLastPart(PARAM_FILENAME_REGEX) ||
178
189
  extractLastPart(HOOKS_FILENAME_REGEX) ||
179
- extractLastPart(/([\w-]+)(\.lazy)?\.svelte$/);
190
+ extractLastPart(FILENAME_REGEX);
180
191
  if (!lastPart) {
181
192
  throw new Error(`Invalid filename: ${value}`);
182
193
  }
@@ -37,7 +37,10 @@ export function writeRouterCode() {
37
37
 
38
38
  console.log('✅️ Routes generated');
39
39
  } catch (error) {
40
- console.error('Error during routes generation:', error);
40
+ console.error(
41
+ 'Error during routes generation:',
42
+ error instanceof Error ? error.message : String(error),
43
+ );
41
44
  }
42
45
  }
43
46
 
@@ -7,25 +7,41 @@ import { constructPath } from './utils.js';
7
7
  * @returns {boolean}
8
8
  */
9
9
  export function isActive(pathname, params) {
10
+ return compare((a, b) => a === b, pathname, params);
11
+ }
12
+
13
+ /**
14
+ * @param {string} pathname
15
+ * @param {Record<string, string>} [params]
16
+ * @returns {boolean}
17
+ */
18
+ isActive.startsWith = (pathname, params) => {
19
+ return compare((a, b) => a.startsWith(b), pathname, params);
20
+ };
21
+
22
+ /**
23
+ * @param {function(string, string): boolean} compareFn
24
+ * @param {string} pathname
25
+ * @param {Record<string, string>} [params]
26
+ * @returns {boolean}
27
+ */
28
+ function compare(compareFn, pathname, params) {
10
29
  if (!pathname.includes(':')) {
11
- return pathname === location.pathname;
30
+ return compareFn(location.pathname, pathname);
12
31
  }
13
32
 
14
33
  if (params) {
15
- return constructPath(pathname, params) === location.pathname;
34
+ return compareFn(location.pathname, constructPath(pathname, params));
16
35
  }
17
36
 
18
37
  const pathParts = pathname.split('/').slice(1);
19
38
  const routeParts = location.pathname.split('/').slice(1);
20
- if (pathParts.length !== routeParts.length) {
21
- return false;
22
- }
23
39
  for (const [index, pathPart] of pathParts.entries()) {
24
40
  const routePart = routeParts[index];
25
41
  if (routePart.startsWith(':')) {
26
42
  continue;
27
43
  }
28
- return pathPart === routePart;
44
+ return compareFn(pathPart, routePart);
29
45
  }
30
46
  return false;
31
47
  }
@@ -59,6 +59,9 @@ export function matchRoute(pathname, routes) {
59
59
  if (param) {
60
60
  params[param] = pathParts.slice(index).join('/');
61
61
  }
62
+ if (breakFromLayouts) {
63
+ routePart = `(${routePart})`;
64
+ }
62
65
  const resolvedPath = /** @type {keyof Routes} */ (
63
66
  (index ? '/' : '') + routeParts.join('/')
64
67
  );
package/src/index.d.ts CHANGED
@@ -63,12 +63,15 @@ export type Hooks = {
63
63
 
64
64
  export type Routes = {
65
65
  [_: `/${string}`]: RouteComponent | Routes;
66
- [_: `*${string}`]: RouteComponent | undefined;
66
+ [_: `*${string}` | `(*${string})`]: RouteComponent | undefined;
67
67
  layout?: LayoutComponent;
68
68
  hooks?: Hooks;
69
69
  };
70
70
 
71
- export type IsActiveLink = Action<HTMLAnchorElement, { className?: string } | undefined>;
71
+ export type IsActiveLink = Action<
72
+ HTMLAnchorElement,
73
+ { className?: string; startsWith?: boolean } | undefined
74
+ >;
72
75
 
73
76
  export type RouterApi<T extends Routes> = {
74
77
  /**
@@ -114,7 +117,10 @@ export type RouterApi<T extends Routes> = {
114
117
  * @param path The route to check.
115
118
  * @param params The optional parameters to replace in the route.
116
119
  */
117
- isActive<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
120
+ isActive: {
121
+ <U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
122
+ startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
123
+ };
118
124
  route: {
119
125
  /**
120
126
  * An object containing the parameters of the current route.
@@ -168,11 +174,13 @@ type NavigateArgs<T extends string> =
168
174
  type StripNonRoutes<T extends Routes> = {
169
175
  [K in keyof T as K extends `*${string}`
170
176
  ? never
171
- : K extends 'layout'
177
+ : K extends `(*${string})`
172
178
  ? never
173
- : K extends 'hooks'
179
+ : K extends 'layout'
174
180
  ? never
175
- : K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
181
+ : K extends 'hooks'
182
+ ? never
183
+ : K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
176
184
  };
177
185
 
178
186
  type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
@@ -191,10 +199,14 @@ type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${in
191
199
 
192
200
  type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
193
201
  ? Param | ExtractParams<`/${Rest}`>
194
- : T extends `${string}:${infer Param}`
202
+ : T extends `${string}(:${infer Param})`
195
203
  ? Param
196
- : T extends `${string}*${infer Param}`
197
- ? Param extends ''
198
- ? never
199
- : Param
200
- : never;
204
+ : T extends `${string}:${infer Param}`
205
+ ? Param
206
+ : T extends `${string}(*${infer Param})`
207
+ ? Param
208
+ : T extends `${string}*${infer Param}`
209
+ ? Param extends ''
210
+ ? never
211
+ : Param
212
+ : never;