sv-router 0.17.0 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Type-safe routing for Svelte SPAs",
5
5
  "keywords": [
6
6
  "router",
@@ -20,6 +20,7 @@
20
20
  "src"
21
21
  ],
22
22
  "type": "module",
23
+ "sideEffects": false,
23
24
  "exports": {
24
25
  ".": {
25
26
  "import": "./src/index.js",
@@ -39,24 +40,24 @@
39
40
  "@eslint/js": "^10.0.1",
40
41
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
41
42
  "@testing-library/jest-dom": "^6.9.1",
42
- "@testing-library/svelte": "^5.3.1",
43
+ "@testing-library/svelte": "^5.4.2",
43
44
  "@testing-library/user-event": "^14.6.1",
44
- "@types/node": "^24.12.4",
45
- "@vitest/coverage-v8": "^4.1.7",
46
- "eslint": "^10.4.0",
45
+ "@types/node": "^24.13.2",
46
+ "@vitest/coverage-v8": "^4.1.9",
47
+ "eslint": "^10.6.0",
47
48
  "eslint-config-prettier": "^10.1.8",
48
49
  "eslint-plugin-simple-import-sort": "^13.0.0",
49
- "eslint-plugin-svelte": "^3.18.0",
50
- "eslint-plugin-unicorn": "^65.0.1",
51
- "globals": "^17.6.0",
52
- "happy-dom": "^20.9.0",
53
- "oxfmt": "^0.52.0",
54
- "svelte-check": "^4.4.8",
50
+ "eslint-plugin-svelte": "^3.20.0",
51
+ "eslint-plugin-unicorn": "^70.0.0",
52
+ "globals": "^17.7.0",
53
+ "happy-dom": "^20.10.6",
54
+ "oxfmt": "^0.57.0",
55
+ "svelte-check": "^4.7.1",
55
56
  "type-testing": "^0.2.0",
56
57
  "typescript": "^6.0.3",
57
- "typescript-eslint": "^8.60.0",
58
- "vite": "^8.0.14",
59
- "vitest": "^4.1.7"
58
+ "typescript-eslint": "^8.62.1",
59
+ "vite": "^8.1.3",
60
+ "vitest": "^4.1.9"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "svelte": "^5"
@@ -12,6 +12,7 @@ import {
12
12
  stripBase,
13
13
  updatedLocation,
14
14
  } from './helpers/utils.js';
15
+ import { validateRoutes } from './helpers/validate-routes.js';
15
16
  import { Navigation } from './navigation.js';
16
17
  import { syncSearchParams } from './search-params.svelte.js';
17
18
 
@@ -53,7 +54,7 @@ let meta = $state({ value: {} });
53
54
  const navigationBlockers = new Map();
54
55
 
55
56
  let historyIndex = 0;
56
- let skipNextPopstate = false;
57
+ let isSkipNextPopstate = false;
57
58
 
58
59
  /** @type {AbortController | null} */
59
60
  let currentNavigationController = null;
@@ -72,7 +73,7 @@ function setBase(basename) {
72
73
  history.replaceState(
73
74
  { _routerIndex: historyIndex, _userState: history.state ?? null },
74
75
  '',
75
- url.toString(),
76
+ url.href,
76
77
  );
77
78
  }
78
79
  } else {
@@ -82,7 +83,7 @@ function setBase(basename) {
82
83
  history.replaceState(
83
84
  { _routerIndex: historyIndex, _userState: history.state ?? null },
84
85
  '',
85
- url.toString(),
86
+ url.href,
86
87
  );
87
88
  }
88
89
  }
@@ -126,9 +127,7 @@ export function createRouter(r, options = {}) {
126
127
  }
127
128
 
128
129
  if (DEV && BROWSER) {
129
- import('./helpers/validate-routes.js').then(({ validateRoutes }) => {
130
- validateRoutes(routes);
131
- });
130
+ validateRoutes(routes);
132
131
  }
133
132
 
134
133
  preloadOnHover(routes);
@@ -140,6 +139,9 @@ export function createRouter(r, options = {}) {
140
139
  async preload(pathname) {
141
140
  await preload(routes, pathname);
142
141
  },
142
+ resolveMeta(pathname) {
143
+ return matchRoute(pathname, routes).meta;
144
+ },
143
145
  route: {
144
146
  get params() {
145
147
  return params.value;
@@ -178,7 +180,7 @@ export function createRouter(r, options = {}) {
178
180
  */
179
181
  async function navigate(path, options = {}) {
180
182
  if (typeof path === 'number') {
181
- globalThis.history.go(path);
183
+ history.go(path);
182
184
  return new Navigation(`History entry: ${path}`);
183
185
  }
184
186
 
@@ -213,8 +215,8 @@ export async function onNavigate(path, options = {}) {
213
215
  throw new Error('Router not initialized: `createRouter` was not called.');
214
216
  }
215
217
 
216
- if (!path && skipNextPopstate) {
217
- skipNextPopstate = false;
218
+ if (!path && isSkipNextPopstate) {
219
+ isSkipNextPopstate = false;
218
220
  return;
219
221
  }
220
222
 
@@ -224,7 +226,7 @@ export async function onNavigate(path, options = {}) {
224
226
  const shouldNavigate = typeof blocker === 'object' ? blocker.onNavigate : blocker;
225
227
  if (!(await shouldNavigate())) {
226
228
  if (!path && popstateDelta !== 0) {
227
- skipNextPopstate = true;
229
+ isSkipNextPopstate = true;
228
230
  history.go(popstateDelta);
229
231
  }
230
232
  return;
@@ -294,10 +296,10 @@ export async function onNavigate(path, options = {}) {
294
296
  }
295
297
  const historyMethod = options.replace ? 'replaceState' : 'pushState';
296
298
  if (historyMethod === 'pushState') historyIndex++;
297
- globalThis.history[historyMethod](
299
+ history[historyMethod](
298
300
  { _routerIndex: historyIndex, _userState: options.state ?? null },
299
301
  '',
300
- url.toString(),
302
+ url.href,
301
303
  );
302
304
  syncSearchParams(search);
303
305
  } else {
@@ -345,6 +347,17 @@ function getMatchPath(path) {
345
347
 
346
348
  /** @param {Event} event */
347
349
  export function onGlobalClick(event) {
350
+ const mouseEvent = /** @type {MouseEvent} */ (event);
351
+ if (
352
+ mouseEvent.button !== 0 ||
353
+ mouseEvent.metaKey ||
354
+ mouseEvent.ctrlKey ||
355
+ mouseEvent.shiftKey ||
356
+ mouseEvent.altKey ||
357
+ mouseEvent.defaultPrevented
358
+ )
359
+ return;
360
+
348
361
  const anchor = /** @type {HTMLElement} */ (event.target).closest('a');
349
362
  if (!anchor) return;
350
363
 
@@ -1,3 +1,4 @@
1
+ /* eslint-disable unicorn/no-unsafe-string-replacement */
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
@@ -174,7 +175,7 @@ function mergeRouteGroup(result, childMap) {
174
175
  }
175
176
  if (hooks) routeWithGroupFiles.hooks = hooks;
176
177
  if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
177
- if (result[key]) {
178
+ if (Object.hasOwn(result, key)) {
178
179
  throw new Error(`Route conflict at \`${key}\``);
179
180
  }
180
181
 
@@ -223,7 +224,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, base, js
223
224
  return result;
224
225
  })(routes, routesPath);
225
226
 
226
- const imports = [...importsMap.entries()].map(([key, value]) => {
227
+ const imports = [...importsMap].map(([key, value]) => {
227
228
  if (value.endsWith('.ts')) {
228
229
  value = value.replace('.ts', '');
229
230
  }
@@ -240,7 +241,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, base, js
240
241
  '',
241
242
  `export const routes = ${stringifiedRoutes};`,
242
243
  ...(js ? [] : ['export type Routes = typeof routes;']),
243
- `export const { p, navigate, isActive, preload, route } = createRouter(routes${
244
+ `export const { p, navigate, isActive, preload, resolveMeta, route } = createRouter(routes${
244
245
  base === undefined ? '' : `, { base: '${base}' }`
245
246
  });`,
246
247
  '',
@@ -12,16 +12,16 @@ export function writeRouterCode() {
12
12
 
13
13
  // Write `.router/tsconfig.json` file
14
14
  const tsMajor = getTypeScriptMajorVersion();
15
- const useBaseUrl = tsMajor === undefined || tsMajor < 6;
15
+ const isBaseUrl = tsMajor === undefined || tsMajor < 6;
16
16
  let alias = genConfig.routerPath;
17
- if (!useBaseUrl) {
17
+ if (!isBaseUrl) {
18
18
  alias = alias.replace(genConfig.genCodeDirPath, '.');
19
19
  }
20
20
  const tsConfig = {
21
21
  compilerOptions: {
22
22
  module: 'preserve',
23
23
  moduleResolution: 'bundler',
24
- ...(useBaseUrl ? { baseUrl: '..' } : {}),
24
+ ...(isBaseUrl && { baseUrl: '..' }),
25
25
  paths: { [genConfig.genCodeAlias]: [alias] },
26
26
  },
27
27
  include: [
@@ -63,10 +63,12 @@ export function writeRouterCode() {
63
63
  * @param {string} content
64
64
  */
65
65
  function writeFileIfDifferent(filePath, content) {
66
- if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
67
- fs.writeFileSync(filePath, content);
68
- return true;
66
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) {
67
+ return;
69
68
  }
69
+
70
+ fs.writeFileSync(filePath, content);
71
+ return true;
70
72
  }
71
73
 
72
74
  function getTypeScriptMajorVersion() {
@@ -34,9 +34,8 @@ function compare(compareFn, pathname, params) {
34
34
  if (params) {
35
35
  if (base.name === '#') {
36
36
  return compareFn(location.pathname, constructPath(pathname, params).replace('/#', ''));
37
- } else {
38
- return compareFn(location.pathname, constructPath(pathname, params));
39
37
  }
38
+ return compareFn(location.pathname, constructPath(pathname, params));
40
39
  }
41
40
 
42
41
  const pathParts = pathname.split('/').slice(1);
@@ -45,10 +44,10 @@ function compare(compareFn, pathname, params) {
45
44
  return false;
46
45
  }
47
46
  for (const [index, pathPart] of pathParts.entries()) {
48
- const routePart = routeParts[index];
49
47
  if (pathPart.startsWith(':')) {
50
48
  continue;
51
49
  }
50
+ const routePart = routeParts[index];
52
51
  if (pathPart !== routePart) {
53
52
  return false;
54
53
  }
@@ -84,11 +84,9 @@ function tryMatch(route, pathParts, pathname, routes, baseMeta) {
84
84
 
85
85
  /** @type {Record<string, string>} */
86
86
  const params = {};
87
- /** @type {boolean} */
88
- let breakFromLayouts;
89
87
 
90
88
  for (let [index, routePart] of routeParts.entries()) {
91
- breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
89
+ const breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
92
90
  if (breakFromLayouts) {
93
91
  routePart = routePart.slice(1, -1);
94
92
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable unicorn/no-break-in-nested-loop */
1
2
  import { matchRoute } from './match-route.js';
2
3
  import { parseSearch, resolveRouteComponents, stripBase } from './utils.js';
3
4
 
@@ -139,10 +140,12 @@ export function preloadOnHover(routes) {
139
140
 
140
141
  const intersectionObserver = new IntersectionObserver((entries) => {
141
142
  for (const entry of entries) {
142
- if (entry.isIntersecting) {
143
- intersectionObserver.unobserve(entry.target);
144
- anchorPreload(/** @type {HTMLAnchorElement} */ (entry.target));
143
+ if (!entry.isIntersecting) {
144
+ continue;
145
145
  }
146
+
147
+ intersectionObserver.unobserve(entry.target);
148
+ anchorPreload(/** @type {HTMLAnchorElement} */ (entry.target));
146
149
  }
147
150
  });
148
151
 
@@ -8,7 +8,7 @@ import { base } from '../create-router.svelte.js';
8
8
  export function constructPath(path, params) {
9
9
  if (params) {
10
10
  for (const key in params) {
11
- path = path.replace(`:${key}`, encodeURIComponent(params[key]));
11
+ path = path.replace(`:${key}`, () => encodeURIComponent(params[key]));
12
12
  }
13
13
  }
14
14
 
@@ -17,7 +17,8 @@ export function constructPath(path, params) {
17
17
  return '/#/';
18
18
  }
19
19
  return join('#', path);
20
- } else if (base.name) {
20
+ }
21
+ if (base.name) {
21
22
  return join(base.name, path);
22
23
  }
23
24
 
@@ -54,16 +55,15 @@ export function resolveRouteComponents(input) {
54
55
  * @param {import('../index.d.ts').RouteComponent} input
55
56
  * @returns {Promise<import('svelte').Component>}
56
57
  */
57
- function resolveRouteComponent(input) {
58
- return new Promise((resolve) => {
59
- if (isLazyImport(input)) {
60
- Promise.resolve(input()).then((module) => {
61
- resolve(module.default);
62
- });
63
- } else {
64
- resolve(input);
58
+ async function resolveRouteComponent(input) {
59
+ if (isLazyImport(input)) {
60
+ const module = await input();
61
+ if (!module) {
62
+ throw new Error('Failed to load route component: the lazy import resolved to nothing');
65
63
  }
66
- });
64
+ return module.default;
65
+ }
66
+ return input;
67
67
  }
68
68
 
69
69
  /**
@@ -112,12 +112,11 @@ export function getUserState(state) {
112
112
  }
113
113
 
114
114
  export function updatedLocation() {
115
- const pathname =
116
- base.name === '#' ? globalThis.location.hash.slice(1) : globalThis.location.pathname;
117
- const hash = base.name === '#' ? '' : globalThis.location.hash;
115
+ const pathname = base.name === '#' ? location.hash.slice(1) : location.pathname;
116
+ const hash = base.name === '#' ? '' : location.hash;
118
117
  return {
119
118
  pathname,
120
- search: globalThis.location.search,
119
+ search: location.search,
121
120
  state: getUserState(history.state),
122
121
  hash,
123
122
  };
@@ -161,7 +160,7 @@ export function parseSearch(value) {
161
160
  if (typeof value === 'string') {
162
161
  const searchParams = new URLSearchParams(value);
163
162
  return Object.fromEntries(
164
- [...searchParams.entries()].map(([key, value]) => [key, parseSearchValue(value)]),
163
+ [...searchParams].map(([key, value]) => [key, parseSearchValue(value)]),
165
164
  );
166
165
  }
167
166
 
package/src/index.d.ts CHANGED
@@ -227,6 +227,13 @@ export type RouterApi<T extends Routes> = {
227
227
  */
228
228
  preload<U extends Path<T>>(path: U): Promise<void>;
229
229
 
230
+ /**
231
+ * Resolves the metadata for the given route path by merging meta from all ancestor route levels.
232
+ *
233
+ * @param path The route to resolve meta for.
234
+ */
235
+ resolveMeta<U extends Path<T>>(path: U): RouteMeta;
236
+
230
237
  route: {
231
238
  /**
232
239
  * An object containing the parameters of the current route.
@@ -1,7 +1,7 @@
1
1
  import { SvelteURLSearchParams } from 'svelte/reactivity';
2
2
  import { parseSearchValue } from './helpers/utils.js';
3
3
 
4
- let searchParams = new SvelteURLSearchParams(globalThis.location.search);
4
+ let searchParams = new SvelteURLSearchParams(location.search);
5
5
 
6
6
  /** @type {import('./index.js').SearchParams} */
7
7
  const shell = {
@@ -38,6 +38,7 @@ const shell = {
38
38
  updateUrlSearchParams(options);
39
39
  },
40
40
  sort(options) {
41
+ // eslint-disable-next-line unicorn/require-array-sort-compare
41
42
  searchParams.sort();
42
43
  updateUrlSearchParams(options);
43
44
  },
@@ -82,7 +83,7 @@ export function syncSearchParams(search) {
82
83
 
83
84
  /** @param {{ replace?: boolean }} [options] */
84
85
  function updateUrlSearchParams(options) {
85
- const url = new URL(globalThis.location.toString());
86
+ const url = new URL(location.toString());
86
87
  url.search = searchParams.toString();
87
- globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
88
+ history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
88
89
  }