sv-router 0.16.2 → 0.17.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.2",
3
+ "version": "0.17.0",
4
4
  "description": "Type-safe routing for Svelte SPAs",
5
5
  "keywords": [
6
6
  "router",
@@ -41,22 +41,22 @@
41
41
  "@testing-library/jest-dom": "^6.9.1",
42
42
  "@testing-library/svelte": "^5.3.1",
43
43
  "@testing-library/user-event": "^14.6.1",
44
- "@types/node": "^24.12.3",
45
- "@vitest/coverage-v8": "^4.1.5",
46
- "eslint": "^10.3.0",
44
+ "@types/node": "^24.12.4",
45
+ "@vitest/coverage-v8": "^4.1.7",
46
+ "eslint": "^10.4.0",
47
47
  "eslint-config-prettier": "^10.1.8",
48
48
  "eslint-plugin-simple-import-sort": "^13.0.0",
49
- "eslint-plugin-svelte": "^3.17.1",
50
- "eslint-plugin-unicorn": "^64.0.0",
49
+ "eslint-plugin-svelte": "^3.18.0",
50
+ "eslint-plugin-unicorn": "^65.0.1",
51
51
  "globals": "^17.6.0",
52
52
  "happy-dom": "^20.9.0",
53
- "oxfmt": "^0.49.0",
53
+ "oxfmt": "^0.52.0",
54
54
  "svelte-check": "^4.4.8",
55
55
  "type-testing": "^0.2.0",
56
56
  "typescript": "^6.0.3",
57
- "typescript-eslint": "^8.59.2",
58
- "vite": "^8.0.11",
59
- "vitest": "^4.1.5"
57
+ "typescript-eslint": "^8.60.0",
58
+ "vite": "^8.0.14",
59
+ "vitest": "^4.1.7"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "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
 
@@ -62,30 +62,42 @@ let pendingController = /** @type {AbortController | null} */ (null);
62
62
  /** @type {Promise<void | null> | null} */
63
63
  let currentNavigationPromise = null;
64
64
 
65
+ /** @param {string} basename */
66
+ function setBase(basename) {
67
+ const url = new URL(globalThis.location.toString());
68
+ if (basename === '#') {
69
+ base.name = '#';
70
+ if (!globalThis.location.href.includes('#')) {
71
+ url.hash = '/';
72
+ history.replaceState(
73
+ { _routerIndex: historyIndex, _userState: history.state ?? null },
74
+ '',
75
+ url.toString(),
76
+ );
77
+ }
78
+ } else {
79
+ base.name = (basename.startsWith('/') ? '' : '/') + basename;
80
+ if (!url.pathname.startsWith(base.name)) {
81
+ url.pathname = join(base.name, url.pathname);
82
+ history.replaceState(
83
+ { _routerIndex: historyIndex, _userState: history.state ?? null },
84
+ '',
85
+ url.toString(),
86
+ );
87
+ }
88
+ }
89
+ Object.assign(location, updatedLocation());
90
+ }
91
+
65
92
  /** @param {string | undefined} basename */
66
93
  export function init(basename) {
67
94
  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
- }
95
+ if (base.name === undefined) {
96
+ setBase(basename);
97
+ } else if (DEV && base.name !== basename && `/${basename}` !== base.name) {
98
+ console.warn(
99
+ 'sv-router: the `base` prop on `<Router>` is ignored because a base was already set in `createRouter`.',
100
+ );
89
101
  }
90
102
  }
91
103
  if (history.state?._routerIndex === undefined) {
@@ -103,11 +115,16 @@ export function init(basename) {
103
115
  /**
104
116
  * @template {import('./index.d.ts').Routes} T
105
117
  * @param {T} r
118
+ * @param {import('./index.d.ts').CreateRouterOptions} [options]
106
119
  * @returns {import('./index.d.ts').RouterApi<T>}
107
120
  */
108
- export function createRouter(r) {
121
+ export function createRouter(r, options = {}) {
109
122
  routes = r;
110
123
 
124
+ if (BROWSER && options.base) {
125
+ setBase(options.base);
126
+ }
127
+
111
128
  if (DEV && BROWSER) {
112
129
  import('./helpers/validate-routes.js').then(({ validateRoutes }) => {
113
130
  validateRoutes(routes);
@@ -229,8 +246,9 @@ export async function onNavigate(path, options = {}) {
229
246
  const matchPath = getMatchPath(path);
230
247
  const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
231
248
 
232
- const search = parseSearch(options.search);
233
- const hooksContext = { pathname: matchPath, meta: newMeta, ...options, search };
249
+ const navContext = path ? options : { ...options, ...updatedLocation() };
250
+ const search = parseSearch(navContext.search);
251
+ const hooksContext = { ...navContext, pathname: matchPath, meta: newMeta, search };
234
252
 
235
253
  let errorHooks = [];
236
254
  for (const hook of hooks) {
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: [],
@@ -19,7 +19,7 @@ const META_FILENAME_REGEX = /(?<=[/.]|^)(meta)(\.svelte)?\.(js|ts)$/; // meta.js
19
19
 
20
20
  /**
21
21
  * @param {string} routesPath
22
- * @param {{ allLazy?: boolean; js?: boolean; ignore?: RegExp[] }} [options]
22
+ * @param {{ allLazy?: boolean; base?: string; js?: boolean; ignore?: RegExp[] }} [options]
23
23
  * @returns {string}
24
24
  */
25
25
  export function generateRouterCode(routesPath, options) {
@@ -149,7 +149,7 @@ function mergeRouteGroup(result, childMap) {
149
149
  const hasRootRoute = '/' in childMap;
150
150
 
151
151
  for (const [key, val] of Object.entries(childMap)) {
152
- if (key === 'layout' || key === 'hooks' || key === 'meta') {
152
+ if (['layout', 'hooks', 'meta'].includes(key)) {
153
153
  continue;
154
154
  }
155
155
 
@@ -185,10 +185,10 @@ function mergeRouteGroup(result, childMap) {
185
185
  /**
186
186
  * @param {GeneratedRoutes} routes
187
187
  * @param {string} routesPath
188
- * @param {{ allLazy?: boolean; js?: boolean }} [options]
188
+ * @param {{ allLazy?: boolean; base?: string; js?: boolean }} [options]
189
189
  * @returns {string}
190
190
  */
191
- export function createRouterCode(routes, routesPath, { allLazy = false, js = false } = {}) {
191
+ export function createRouterCode(routes, routesPath, { allLazy = false, base, js = false } = {}) {
192
192
  if (!routesPath.endsWith('/')) {
193
193
  routesPath += '/';
194
194
  }
@@ -240,7 +240,9 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
240
240
  '',
241
241
  `export const routes = ${stringifiedRoutes};`,
242
242
  ...(js ? [] : ['export type Routes = typeof routes;']),
243
- 'export const { p, navigate, isActive, preload, route } = createRouter(routes);',
243
+ `export const { p, navigate, isActive, preload, route } = createRouter(routes${
244
+ base === undefined ? '' : `, { base: '${base}' }`
245
+ });`,
244
246
  '',
245
247
  ].join('\n');
246
248
  }
@@ -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
  });
@@ -73,7 +74,7 @@ function getTypeScriptMajorVersion() {
73
74
  const require = createRequire(process.cwd() + '/package.json');
74
75
  const tsPackagePath = require.resolve('typescript/package.json');
75
76
  const tsPackage = JSON.parse(fs.readFileSync(tsPackagePath, 'utf8'));
76
- return Number(tsPackage.version.split('.')[0]);
77
+ return Number(tsPackage.version.split('.', 1)[0]);
77
78
  } catch {
78
79
  return;
79
80
  }
@@ -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
 
@@ -68,7 +68,7 @@ function resolveRouteComponent(input) {
68
68
 
69
69
  /**
70
70
  * @param {unknown} input
71
- * @returns {input is import("../index.d.ts").LazyRouteComponent}
71
+ * @returns {input is import('../index.d.ts').LazyRouteComponent}
72
72
  */
73
73
  export function isLazyImport(input) {
74
74
  return (
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
@@ -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';