sv-router 0.11.0 → 0.12.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.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Modern Svelte Routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -36,29 +36,29 @@
36
36
  "esm-env": "^1.2.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@changesets/cli": "^2.29.7",
40
- "@eslint/js": "^9.39.0",
39
+ "@changesets/cli": "^2.29.8",
40
+ "@eslint/js": "^9.39.1",
41
41
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
42
42
  "@testing-library/jest-dom": "^6.9.1",
43
- "@testing-library/svelte": "^5.2.8",
43
+ "@testing-library/svelte": "^5.2.9",
44
44
  "@testing-library/user-event": "^14.6.1",
45
- "@types/node": "^24.9.2",
46
- "eslint": "^9.39.0",
45
+ "@types/node": "^24.10.1",
46
+ "eslint": "^9.39.1",
47
47
  "eslint-config-prettier": "^10.1.8",
48
48
  "eslint-plugin-simple-import-sort": "^12.1.1",
49
- "eslint-plugin-svelte": "^3.13.0",
49
+ "eslint-plugin-svelte": "^3.13.1",
50
50
  "eslint-plugin-unicorn": "^62.0.0",
51
51
  "globals": "^16.5.0",
52
- "happy-dom": "^20.0.10",
53
- "prettier": "^3.6.2",
54
- "prettier-plugin-jsdoc": "^1.5.0",
52
+ "happy-dom": "^20.0.11",
53
+ "prettier": "^3.7.4",
54
+ "prettier-plugin-jsdoc": "^1.8.0",
55
55
  "prettier-plugin-svelte": "^3.4.0",
56
- "svelte-check": "^4.3.3",
56
+ "svelte-check": "^4.3.4",
57
57
  "type-testing": "^0.2.0",
58
58
  "typescript": "^5.9.3",
59
- "typescript-eslint": "^8.46.2",
60
- "vite": "^7.1.12",
61
- "vitest": "^4.0.6"
59
+ "typescript-eslint": "^8.48.1",
60
+ "vite": "^7.2.6",
61
+ "vitest": "^4.0.15"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "svelte": "^5"
package/src/cli/index.js CHANGED
@@ -1,13 +1,76 @@
1
1
  #!/usr/bin/env node
2
+ /* eslint-disable no-console */
2
3
 
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import path from 'node:path';
3
6
  import { genConfig } from '../gen/config.js';
4
7
  import { writeRouterCode } from '../gen/write-router-code.js';
5
8
 
6
9
  const args = process.argv.slice(2).flatMap((arg) => arg.split('='));
7
10
 
11
+ if (args.length > 0) {
12
+ parseArgs();
13
+ } else {
14
+ parseViteConfig();
15
+ }
16
+
17
+ writeRouterCode();
18
+
19
+ function parseViteConfig() {
20
+ const viteConfig = readViteConfig('ts') || readViteConfig('js');
21
+ if (!viteConfig) return;
22
+ const routerConfig = extractRouterConfig(viteConfig);
23
+ if (!routerConfig) return;
24
+ console.log('ℹ️ Using router plugin options from Vite config');
25
+ if (routerConfig.allLazy) genConfig.allLazy = routerConfig.allLazy;
26
+ if (routerConfig.js) genConfig.routesInJs = routerConfig.js;
27
+ if (routerConfig.path) genConfig.routesPath = routerConfig.path;
28
+ if (routerConfig.ignore) genConfig.ignore = routerConfig.ignore;
29
+ }
30
+
31
+ /**
32
+ * @param {'js' | 'ts'} extension
33
+ * @returns {string | undefined}
34
+ */
35
+ function readViteConfig(extension) {
36
+ const vitePath = path.join(process.cwd(), 'vite.config.' + extension);
37
+ if (existsSync(vitePath)) {
38
+ return readFileSync(vitePath, 'utf8');
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {string} viteConfig
44
+ * @returns {import('../vite-plugin/index.d.ts').RouterOptions | undefined}
45
+ */
46
+ function extractRouterConfig(viteConfig) {
47
+ const regex = /router\(\s*([^)]+)\)/;
48
+ const match = viteConfig.match(regex);
49
+ if (!match) return;
50
+ try {
51
+ return new Function(`return ${match[1]}`)();
52
+ } catch (error) {
53
+ console.error('⚠️ Error parsing router config:', error);
54
+ }
55
+ }
56
+
57
+ function parseArgs() {
58
+ const allLazyArg = arg('allLazy');
59
+ if (allLazyArg) genConfig.allLazy = true;
60
+
61
+ const jsArg = arg('js');
62
+ if (jsArg) genConfig.routesInJs = true;
63
+
64
+ const pathArg = arg('path');
65
+ if (pathArg) genConfig.routesPath = pathArg;
66
+
67
+ const ignoreArg = arg('ignore');
68
+ if (ignoreArg) genConfig.ignore = ignoreArg.split(',').map((ignore) => new RegExp(ignore, 'gu'));
69
+ }
70
+
8
71
  /**
9
72
  * @param {keyof import('../vite-plugin/index.d.ts').RouterOptions} option
10
- * @returns
73
+ * @returns {string | undefined}
11
74
  */
12
75
  function arg(option) {
13
76
  const pathArgIndex = args.indexOf('--' + option);
@@ -17,17 +80,3 @@ function arg(option) {
17
80
  }
18
81
  return args[pathArgIndex];
19
82
  }
20
-
21
- const allLazyArg = arg('allLazy');
22
- if (allLazyArg) genConfig.allLazy = true;
23
-
24
- const jsArg = arg('js');
25
- if (jsArg) genConfig.routesInJs = true;
26
-
27
- const pathArg = arg('path');
28
- if (pathArg) genConfig.routesPath = pathArg;
29
-
30
- const ignoreArg = arg('ignore');
31
- if (ignoreArg) genConfig.ignore = ignoreArg.split(',').map((ignore) => new RegExp(ignore, 'gu'));
32
-
33
- writeRouterCode();
@@ -134,7 +134,7 @@ function navigate(path, options = {}) {
134
134
 
135
135
  path = constructPath(path, options.params);
136
136
  if (base.name === '#') {
137
- path = new URL(path).hash;
137
+ path = path.replace('/#', '');
138
138
  } else if (options.hash && !options.hash.startsWith('#')) {
139
139
  options.hash = '#' + options.hash;
140
140
  }
@@ -200,8 +200,10 @@ export async function onNavigate(path, options = {}) {
200
200
  url.hash = options.hash || '';
201
201
  if (base.name === '#') {
202
202
  url.hash = path;
203
+ } else if (base.name && !path.startsWith(base.name)) {
204
+ url.pathname = join(base.name, path);
203
205
  } else {
204
- url.pathname = base.name ? join(base.name, path) : path;
206
+ url.pathname = path;
205
207
  }
206
208
  const historyMethod = options.replace ? 'replaceState' : 'pushState';
207
209
  globalThis.history[historyMethod](options.state || {}, '', url.toString());
@@ -5,11 +5,12 @@ import path from 'node:path';
5
5
 
6
6
  /**
7
7
  * @typedef {{
8
- * [key: string]: string | GeneratedRoutes;
8
+ * [key: string]: string | string[] | GeneratedRoutes;
9
9
  * }} GeneratedRoutes
10
10
  */
11
11
 
12
12
  const FILENAME_REGEX = /(?<=[/.]|^)\(?([\w-]+)\)?(\.lazy)?\.svelte$/; // any.svelte, any.lazy.svelte, (any).svelte
13
+ const INDEX_FILENAME_REGEX = /(?<=[/.]|^)\(?index\)?(\.lazy)?\.svelte$/; // index.svelte, index.lazy.svelte, (index).svelte
13
14
  const PARAM_FILENAME_REGEX = /(?<=[/.]|^)\(?\[([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte, ([any]).svelte
14
15
  const CATCH_ALL_FILENAME_REGEX = /(?<=[/.]|^)\(?\[\.\.\.([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
15
16
  const OUT_OF_LAYOUT_FILENAME_REGEX = /(?<=[/.]|^)\(\[\.?\.?\.?([\w-]+)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
@@ -81,8 +82,9 @@ export function createRouteMap(fileTree, prefix = '') {
81
82
  continue;
82
83
  }
83
84
 
84
- if (entry.endsWith('index.svelte') || entry.endsWith('index.lazy.svelte')) {
85
- const indexEntry = entry.replace(/\.?index(\.lazy)?\.svelte/, '');
85
+ if (INDEX_FILENAME_REGEX.test(entry)) {
86
+ const replacement = /\.?\(index\)(\.lazy)?\.svelte/.test(entry) ? '()' : '';
87
+ const indexEntry = entry.replace(/\.?\(?index\)?(\.lazy)?\.svelte/, replacement);
86
88
  result['/' + (indexEntry ? filePathToRoute(indexEntry) : '')] = prefix + entry;
87
89
  continue;
88
90
  }
@@ -144,22 +146,28 @@ function mergeRouteGroup(result, childMap) {
144
146
  const layout = childMap.layout;
145
147
  const hooks = childMap.hooks;
146
148
  const meta = childMap.meta;
149
+ const hasRootRoute = '/' in childMap;
147
150
 
148
151
  for (const [key, val] of Object.entries(childMap)) {
149
152
  if (key === 'layout' || key === 'hooks' || key === 'meta') {
150
153
  continue;
151
154
  }
152
155
 
156
+ const childMeta =
157
+ typeof val === 'object' && !Array.isArray(val) && 'meta' in val ? val.meta : undefined;
158
+ const mergedMeta =
159
+ childMeta && meta && !hasRootRoute ? [childMeta, meta].flat() : childMeta || meta;
160
+
153
161
  /** @type {GeneratedRoutes} */
154
162
  let routeWithGroupFiles = {};
155
163
  if (typeof val === 'string') {
156
164
  routeWithGroupFiles = { '/': val };
157
- } else {
165
+ } else if (!Array.isArray(val)) {
158
166
  routeWithGroupFiles = { ...val };
159
167
  }
160
- if (layout) routeWithGroupFiles.layout = layout;
161
- if (hooks) routeWithGroupFiles.hooks = hooks;
162
- if (meta) routeWithGroupFiles.meta = meta;
168
+ if (layout) routeWithGroupFiles.layout = /** @type {string} */ (layout);
169
+ if (hooks) routeWithGroupFiles.hooks = /** @type {string} */ (hooks);
170
+ if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
163
171
  if (result[key]) {
164
172
  throw new Error(`Route conflict at \`${key}\``);
165
173
  }
@@ -186,12 +194,18 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
186
194
  /** @type {GeneratedRoutes} */
187
195
  const result = {};
188
196
  for (const [key, value] of Object.entries(routes)) {
189
- if (typeof value === 'object') {
197
+ if (typeof value === 'object' && !Array.isArray(value)) {
190
198
  result[key] = handleImports(value, routesPath);
199
+ } else if (key === 'meta' && Array.isArray(value)) {
200
+ const varNames = value.map((metaPath) => {
201
+ const variableName = pathToCorrectCasing(metaPath);
202
+ importsMap.set(variableName, routesPath + metaPath);
203
+ return variableName;
204
+ });
205
+ result[key] = `{ ...${varNames.toReversed().join(', ...')} }`;
191
206
  } else if (
192
- key === 'hooks' ||
193
- key === 'meta' ||
194
- (!value.endsWith('.lazy.svelte') && !allLazy)
207
+ typeof value === 'string' &&
208
+ (key === 'hooks' || key === 'meta' || (!value.endsWith('.lazy.svelte') && !allLazy))
195
209
  ) {
196
210
  const variableName = pathToCorrectCasing(value);
197
211
  importsMap.set(variableName, routesPath + value);
@@ -40,13 +40,13 @@ export function writeRouterCode() {
40
40
  const written = writeFileIfDifferent(genConfig.routerPath, routerCode);
41
41
 
42
42
  if (written) {
43
- console.log('✅️ Routes generated');
43
+ console.log('❇️ Routes generated');
44
44
  } else {
45
45
  console.log('✅️ Routes already up to date');
46
46
  }
47
47
  } catch (error) {
48
48
  console.error(
49
- 'Error during routes generation:',
49
+ '⚠️ Error during routes generation:',
50
50
  error instanceof Error ? error.message : String(error),
51
51
  );
52
52
  }
@@ -1,5 +1,5 @@
1
1
  import { base, location } from '../create-router.svelte.js';
2
- import { constructPath, join } from './utils.js';
2
+ import { constructPath } from './utils.js';
3
3
 
4
4
  /**
5
5
  * @param {string} pathname
@@ -7,8 +7,7 @@ import { constructPath, join } from './utils.js';
7
7
  * @returns {boolean}
8
8
  */
9
9
  export function isActive(pathname, params) {
10
- const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
11
- return compare((a, b) => a === b, p, params);
10
+ return compare((a, b) => a === b, pathname, params);
12
11
  }
13
12
 
14
13
  /**
@@ -17,8 +16,7 @@ export function isActive(pathname, params) {
17
16
  * @returns {boolean}
18
17
  */
19
18
  isActive.startsWith = (pathname, params) => {
20
- const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
21
- return compare((a, b) => a.startsWith(b), p, params);
19
+ return compare((a, b) => a.startsWith(b), pathname, params);
22
20
  };
23
21
 
24
22
  /**
@@ -34,7 +32,7 @@ function compare(compareFn, pathname, params) {
34
32
 
35
33
  if (params) {
36
34
  if (base.name === '#') {
37
- return compareFn(location.pathname, new URL(constructPath(pathname, params)).hash.slice(1));
35
+ return compareFn(location.pathname, constructPath(pathname, params).replace('/#', ''));
38
36
  } else {
39
37
  return compareFn(location.pathname, constructPath(pathname, params));
40
38
  }
@@ -45,6 +45,11 @@ export function matchRoute(pathname, routes) {
45
45
  /** @type {RouteMeta} */
46
46
  let meta = {};
47
47
 
48
+ const rootRoute = routes['/'];
49
+ if (rootRoute && typeof rootRoute === 'object' && 'meta' in rootRoute && rootRoute.meta) {
50
+ meta = { ...meta, ...rootRoute.meta };
51
+ }
52
+
48
53
  let breakFromLayouts = false;
49
54
 
50
55
  outer: for (const route of allRoutes) {
@@ -76,7 +81,7 @@ export function matchRoute(pathname, routes) {
76
81
  );
77
82
  match = /** @type {RouteComponent} */ (routes[resolvedPath]);
78
83
  break outer;
79
- } else if (routePart !== pathPart?.toLowerCase()) {
84
+ } else if (routePart.toLowerCase() !== pathPart?.toLowerCase()) {
80
85
  break;
81
86
  }
82
87
 
@@ -88,6 +93,10 @@ export function matchRoute(pathname, routes) {
88
93
  routes[/** @type {keyof Routes} */ ('/' + routeParts.join('/'))]
89
94
  );
90
95
 
96
+ if (typeof routeMatch === 'function' && routeParts.length !== pathParts.length) {
97
+ continue;
98
+ }
99
+
91
100
  if (!breakFromLayouts && 'layout' in routes && routes.layout) {
92
101
  layouts.push(routes.layout);
93
102
  }
@@ -13,11 +13,12 @@ export function constructPath(path, params) {
13
13
  }
14
14
 
15
15
  if (base.name === '#') {
16
- const url = new URL(globalThis.location.toString());
17
- url.hash = path;
18
- url.search = '';
19
-
20
- return url.toString();
16
+ if (path === '/') {
17
+ return '/#/';
18
+ }
19
+ return join('#', path);
20
+ } else if (base.name) {
21
+ return join(base.name, path);
21
22
  }
22
23
 
23
24
  return path;