sv-router 0.16.3 → 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.16.3",
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": "^64.0.0",
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"
package/src/cli/index.js CHANGED
@@ -23,6 +23,7 @@ function parseViteConfig() {
23
23
  if (!routerConfig) return;
24
24
  console.log('ℹ️ Using router plugin options from Vite config');
25
25
  if (routerConfig.allLazy) genConfig.allLazy = routerConfig.allLazy;
26
+ if (routerConfig.base) genConfig.base = routerConfig.base;
26
27
  if (routerConfig.js) genConfig.routesInJs = routerConfig.js;
27
28
  if (routerConfig.path) genConfig.routesPath = routerConfig.path;
28
29
  if (routerConfig.ignore) genConfig.ignore = routerConfig.ignore;
@@ -58,6 +59,9 @@ function parseArgs() {
58
59
  const allLazyArg = arg('allLazy');
59
60
  if (allLazyArg) genConfig.allLazy = true;
60
61
 
62
+ const baseArg = arg('base');
63
+ if (baseArg) genConfig.base = baseArg;
64
+
61
65
  const jsArg = arg('js');
62
66
  if (jsArg) genConfig.routesInJs = true;
63
67
 
@@ -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;
@@ -62,30 +63,42 @@ let pendingController = /** @type {AbortController | null} */ (null);
62
63
  /** @type {Promise<void | null> | null} */
63
64
  let currentNavigationPromise = null;
64
65
 
66
+ /** @param {string} basename */
67
+ function setBase(basename) {
68
+ const url = new URL(globalThis.location.toString());
69
+ if (basename === '#') {
70
+ base.name = '#';
71
+ if (!globalThis.location.href.includes('#')) {
72
+ url.hash = '/';
73
+ history.replaceState(
74
+ { _routerIndex: historyIndex, _userState: history.state ?? null },
75
+ '',
76
+ url.href,
77
+ );
78
+ }
79
+ } else {
80
+ base.name = (basename.startsWith('/') ? '' : '/') + basename;
81
+ if (!url.pathname.startsWith(base.name)) {
82
+ url.pathname = join(base.name, url.pathname);
83
+ history.replaceState(
84
+ { _routerIndex: historyIndex, _userState: history.state ?? null },
85
+ '',
86
+ url.href,
87
+ );
88
+ }
89
+ }
90
+ Object.assign(location, updatedLocation());
91
+ }
92
+
65
93
  /** @param {string | undefined} basename */
66
94
  export function init(basename) {
67
95
  if (basename) {
68
- const url = new URL(globalThis.location.toString());
69
- if (basename === '#') {
70
- base.name = '#';
71
- if (!globalThis.location.href.includes('#')) {
72
- url.hash = '/';
73
- history.replaceState(
74
- { _routerIndex: historyIndex, _userState: history.state ?? null },
75
- '',
76
- url.toString(),
77
- );
78
- }
79
- } else {
80
- base.name = (basename.startsWith('/') ? '' : '/') + basename;
81
- if (!url.pathname.startsWith(base.name)) {
82
- url.pathname = join(base.name, url.pathname);
83
- history.replaceState(
84
- { _routerIndex: historyIndex, _userState: history.state ?? null },
85
- '',
86
- url.toString(),
87
- );
88
- }
96
+ if (base.name === undefined) {
97
+ setBase(basename);
98
+ } else if (DEV && base.name !== basename && `/${basename}` !== base.name) {
99
+ console.warn(
100
+ 'sv-router: the `base` prop on `<Router>` is ignored because a base was already set in `createRouter`.',
101
+ );
89
102
  }
90
103
  }
91
104
  if (history.state?._routerIndex === undefined) {
@@ -103,15 +116,18 @@ export function init(basename) {
103
116
  /**
104
117
  * @template {import('./index.d.ts').Routes} T
105
118
  * @param {T} r
119
+ * @param {import('./index.d.ts').CreateRouterOptions} [options]
106
120
  * @returns {import('./index.d.ts').RouterApi<T>}
107
121
  */
108
- export function createRouter(r) {
122
+ export function createRouter(r, options = {}) {
109
123
  routes = r;
110
124
 
125
+ if (BROWSER && options.base) {
126
+ setBase(options.base);
127
+ }
128
+
111
129
  if (DEV && BROWSER) {
112
- import('./helpers/validate-routes.js').then(({ validateRoutes }) => {
113
- validateRoutes(routes);
114
- });
130
+ validateRoutes(routes);
115
131
  }
116
132
 
117
133
  preloadOnHover(routes);
@@ -123,6 +139,9 @@ export function createRouter(r) {
123
139
  async preload(pathname) {
124
140
  await preload(routes, pathname);
125
141
  },
142
+ resolveMeta(pathname) {
143
+ return matchRoute(pathname, routes).meta;
144
+ },
126
145
  route: {
127
146
  get params() {
128
147
  return params.value;
@@ -161,7 +180,7 @@ export function createRouter(r) {
161
180
  */
162
181
  async function navigate(path, options = {}) {
163
182
  if (typeof path === 'number') {
164
- globalThis.history.go(path);
183
+ history.go(path);
165
184
  return new Navigation(`History entry: ${path}`);
166
185
  }
167
186
 
@@ -196,8 +215,8 @@ export async function onNavigate(path, options = {}) {
196
215
  throw new Error('Router not initialized: `createRouter` was not called.');
197
216
  }
198
217
 
199
- if (!path && skipNextPopstate) {
200
- skipNextPopstate = false;
218
+ if (!path && isSkipNextPopstate) {
219
+ isSkipNextPopstate = false;
201
220
  return;
202
221
  }
203
222
 
@@ -207,7 +226,7 @@ export async function onNavigate(path, options = {}) {
207
226
  const shouldNavigate = typeof blocker === 'object' ? blocker.onNavigate : blocker;
208
227
  if (!(await shouldNavigate())) {
209
228
  if (!path && popstateDelta !== 0) {
210
- skipNextPopstate = true;
229
+ isSkipNextPopstate = true;
211
230
  history.go(popstateDelta);
212
231
  }
213
232
  return;
@@ -277,10 +296,10 @@ export async function onNavigate(path, options = {}) {
277
296
  }
278
297
  const historyMethod = options.replace ? 'replaceState' : 'pushState';
279
298
  if (historyMethod === 'pushState') historyIndex++;
280
- globalThis.history[historyMethod](
299
+ history[historyMethod](
281
300
  { _routerIndex: historyIndex, _userState: options.state ?? null },
282
301
  '',
283
- url.toString(),
302
+ url.href,
284
303
  );
285
304
  syncSearchParams(search);
286
305
  } else {
@@ -328,6 +347,17 @@ function getMatchPath(path) {
328
347
 
329
348
  /** @param {Event} event */
330
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
+
331
361
  const anchor = /** @type {HTMLElement} */ (event.target).closest('a');
332
362
  if (!anchor) return;
333
363
 
package/src/gen/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @type {{
3
3
  * allLazy: boolean;
4
+ * base: string | undefined;
4
5
  * ignore: RegExp[];
5
6
  * readonly genCodeAlias: string;
6
7
  * readonly genCodeDirPath: string;
@@ -12,6 +13,7 @@
12
13
  */
13
14
  export const genConfig = {
14
15
  allLazy: false,
16
+ base: undefined,
15
17
  genCodeAlias: 'sv-router/generated',
16
18
  genCodeDirPath: '.router',
17
19
  ignore: [],
@@ -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
 
@@ -19,7 +20,7 @@ const META_FILENAME_REGEX = /(?<=[/.]|^)(meta)(\.svelte)?\.(js|ts)$/; // meta.js
19
20
 
20
21
  /**
21
22
  * @param {string} routesPath
22
- * @param {{ allLazy?: boolean; js?: boolean; ignore?: RegExp[] }} [options]
23
+ * @param {{ allLazy?: boolean; base?: string; js?: boolean; ignore?: RegExp[] }} [options]
23
24
  * @returns {string}
24
25
  */
25
26
  export function generateRouterCode(routesPath, options) {
@@ -149,7 +150,7 @@ function mergeRouteGroup(result, childMap) {
149
150
  const hasRootRoute = '/' in childMap;
150
151
 
151
152
  for (const [key, val] of Object.entries(childMap)) {
152
- if (key === 'layout' || key === 'hooks' || key === 'meta') {
153
+ if (['layout', 'hooks', 'meta'].includes(key)) {
153
154
  continue;
154
155
  }
155
156
 
@@ -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
 
@@ -185,10 +186,10 @@ function mergeRouteGroup(result, childMap) {
185
186
  /**
186
187
  * @param {GeneratedRoutes} routes
187
188
  * @param {string} routesPath
188
- * @param {{ allLazy?: boolean; js?: boolean }} [options]
189
+ * @param {{ allLazy?: boolean; base?: string; js?: boolean }} [options]
189
190
  * @returns {string}
190
191
  */
191
- export function createRouterCode(routes, routesPath, { allLazy = false, js = false } = {}) {
192
+ export function createRouterCode(routes, routesPath, { allLazy = false, base, js = false } = {}) {
192
193
  if (!routesPath.endsWith('/')) {
193
194
  routesPath += '/';
194
195
  }
@@ -223,7 +224,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
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,9 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
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${
245
+ base === undefined ? '' : `, { base: '${base}' }`
246
+ });`,
244
247
  '',
245
248
  ].join('\n');
246
249
  }
@@ -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: [
@@ -39,6 +39,7 @@ export function writeRouterCode() {
39
39
  // Write `.router/router.ts` file
40
40
  const routerCode = generateRouterCode(genConfig.routesPath, {
41
41
  allLazy: genConfig.allLazy,
42
+ base: genConfig.base,
42
43
  js: genConfig.routesInJs,
43
44
  ignore: genConfig.ignore,
44
45
  });
@@ -62,10 +63,12 @@ export function writeRouterCode() {
62
63
  * @param {string} content
63
64
  */
64
65
  function writeFileIfDifferent(filePath, content) {
65
- if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
66
- fs.writeFileSync(filePath, content);
67
- return true;
66
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) {
67
+ return;
68
68
  }
69
+
70
+ fs.writeFileSync(filePath, content);
71
+ return true;
69
72
  }
70
73
 
71
74
  function getTypeScriptMajorVersion() {
@@ -73,7 +76,7 @@ function getTypeScriptMajorVersion() {
73
76
  const require = createRequire(process.cwd() + '/package.json');
74
77
  const tsPackagePath = require.resolve('typescript/package.json');
75
78
  const tsPackage = JSON.parse(fs.readFileSync(tsPackagePath, 'utf8'));
76
- return Number(tsPackage.version.split('.')[0]);
79
+ return Number(tsPackage.version.split('.', 1)[0]);
77
80
  } catch {
78
81
  return;
79
82
  }
@@ -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}`, String(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
@@ -26,8 +26,19 @@ export function serializeSearch(search: Search): string | undefined;
26
26
  * ...
27
27
  * });
28
28
  * ```
29
+ *
30
+ * A base path can be provided as an option:
31
+ *
32
+ * ```js
33
+ * export const { p, ... } = createRouter({ ... }, { base: 'my-app' });
34
+ * ```
29
35
  */
30
- export function createRouter<T extends Routes>(r: T): RouterApi<T>;
36
+ export function createRouter<T extends Routes>(r: T, options?: CreateRouterOptions): RouterApi<T>;
37
+
38
+ export type CreateRouterOptions = {
39
+ /** The base path that is prepended to every URL. Use `'#'` to enable hash-based routing. */
40
+ base?: string;
41
+ };
31
42
 
32
43
  /**
33
44
  * Blocks navigation as long as the callback returns `false`.
@@ -66,11 +77,16 @@ export function blockNavigation(
66
77
  | { beforeUnload?(): boolean; onNavigate(): boolean | Promise<boolean> },
67
78
  ): () => void;
68
79
 
69
- /**
70
- * The component that will render the current route. You can pass a `base` prop to set the base path
71
- * that is prepended to every url.
72
- */
73
- export const Router: Component<{ base?: string }>;
80
+ /** The component that will render the current route. */
81
+ export const Router: Component<{
82
+ /**
83
+ * The base path that is prepended to every URL.
84
+ *
85
+ * @deprecated Use the `base` option of `createRouter` (or of the Vite plugin for file-based
86
+ * routing) instead.
87
+ */
88
+ base?: string;
89
+ }>;
74
90
 
75
91
  /**
76
92
  * The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
@@ -211,6 +227,13 @@ export type RouterApi<T extends Routes> = {
211
227
  */
212
228
  preload<U extends Path<T>>(path: U): Promise<void>;
213
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
+
214
237
  route: {
215
238
  /**
216
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
  }
@@ -7,6 +7,12 @@ export type RouterOptions = {
7
7
  * @default false
8
8
  */
9
9
  allLazy?: boolean;
10
+ /**
11
+ * The base path that is prepended to every URL. Use `'#'` to enable hash-based routing.
12
+ *
13
+ * @default undefined
14
+ */
15
+ base?: string;
10
16
  /**
11
17
  * If true, generates the routes in a .js file instead of a .ts file.
12
18
  *
@@ -8,6 +8,7 @@ import { writeRouterCode } from '../gen/write-router-code.js';
8
8
  */
9
9
  export function router(options) {
10
10
  genConfig.allLazy = options?.allLazy || false;
11
+ genConfig.base = options?.base;
11
12
  genConfig.ignore = options?.ignore || [];
12
13
  genConfig.routesInJs = options?.js || false;
13
14
  genConfig.routesPath = options?.path || 'src/routes';