sv-router 0.10.5 → 0.11.1

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.10.5",
3
+ "version": "0.11.1",
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
@@ -27,4 +27,7 @@ if (jsArg) genConfig.routesInJs = true;
27
27
  const pathArg = arg('path');
28
28
  if (pathArg) genConfig.routesPath = pathArg;
29
29
 
30
+ const ignoreArg = arg('ignore');
31
+ if (ignoreArg) genConfig.ignore = ignoreArg.split(',').map((ignore) => new RegExp(ignore, 'gu'));
32
+
30
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());
package/src/gen/config.js CHANGED
@@ -1,22 +1,24 @@
1
1
  /**
2
2
  * @type {{
3
3
  * allLazy: boolean;
4
- * routesInJs: boolean;
5
- * routesPath: string;
4
+ * ignore: RegExp[];
5
+ * readonly genCodeAlias: string;
6
6
  * readonly genCodeDirPath: string;
7
7
  * readonly routerPath: string;
8
8
  * readonly tsconfigPath: string;
9
- * readonly genCodeAlias: string;
9
+ * routesInJs: boolean;
10
+ * routesPath: string;
10
11
  * }}
11
12
  */
12
13
  export const genConfig = {
13
14
  allLazy: false,
14
- routesInJs: false,
15
- routesPath: 'src/routes',
15
+ genCodeAlias: 'sv-router/generated',
16
16
  genCodeDirPath: '.router',
17
+ ignore: [],
17
18
  get routerPath() {
18
19
  return '.router/router.' + (this.routesInJs ? 'js' : 'ts');
19
20
  },
21
+ routesInJs: false,
22
+ routesPath: 'src/routes',
20
23
  tsconfigPath: '.router/tsconfig.json',
21
- genCodeAlias: 'sv-router/generated',
22
24
  };
@@ -5,7 +5,7 @@ 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
 
@@ -18,7 +18,7 @@ const META_FILENAME_REGEX = /(?<=[/.]|^)(meta)(\.svelte)?\.(js|ts)$/; // meta.js
18
18
 
19
19
  /**
20
20
  * @param {string} routesPath
21
- * @param {{ allLazy?: boolean; js?: boolean }} [options]
21
+ * @param {{ allLazy?: boolean; js?: boolean; ignore?: RegExp[] }} [options]
22
22
  * @returns {string}
23
23
  */
24
24
  export function generateRouterCode(routesPath, options) {
@@ -26,29 +26,31 @@ export function generateRouterCode(routesPath, options) {
26
26
  if (!fs.existsSync(absoluteRoutesPath)) {
27
27
  throw new Error(`Routes directory not found at \`${routesPath}\``);
28
28
  }
29
- const fileTree = buildFileTree(absoluteRoutesPath);
29
+ const fileTree = buildFileTree(absoluteRoutesPath, options?.ignore ?? []);
30
30
  const routeMap = createRouteMap(fileTree);
31
31
  return createRouterCode(routeMap, path.posix.join('..', routesPath), options);
32
32
  }
33
33
 
34
34
  /**
35
35
  * @param {string} routesPath
36
+ * @param {RegExp[]} ignores
36
37
  * @returns {FileTree}
37
38
  */
38
- export function buildFileTree(routesPath) {
39
+ export function buildFileTree(routesPath, ignores) {
39
40
  const entries = fs.readdirSync(routesPath);
40
41
  /** @type {FileTree} */
41
42
  const tree = [];
42
43
  for (const entry of entries) {
43
44
  const stat = fs.lstatSync(path.join(routesPath, entry));
44
45
  if (stat.isDirectory()) {
45
- tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
46
+ tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry), ignores) });
46
47
  continue;
47
48
  }
48
49
  if (
49
- !entry.endsWith('.svelte') &&
50
- !HOOKS_FILENAME_REGEX.test(entry) &&
51
- !META_FILENAME_REGEX.test(entry)
50
+ (!entry.endsWith('.svelte') &&
51
+ !HOOKS_FILENAME_REGEX.test(entry) &&
52
+ !META_FILENAME_REGEX.test(entry)) ||
53
+ ignores.some((ignore) => ignore.test(entry))
52
54
  ) {
53
55
  continue;
54
56
  }
@@ -142,22 +144,28 @@ function mergeRouteGroup(result, childMap) {
142
144
  const layout = childMap.layout;
143
145
  const hooks = childMap.hooks;
144
146
  const meta = childMap.meta;
147
+ const hasRootRoute = '/' in childMap;
145
148
 
146
149
  for (const [key, val] of Object.entries(childMap)) {
147
150
  if (key === 'layout' || key === 'hooks' || key === 'meta') {
148
151
  continue;
149
152
  }
150
153
 
154
+ const childMeta =
155
+ typeof val === 'object' && !Array.isArray(val) && 'meta' in val ? val.meta : undefined;
156
+ const mergedMeta =
157
+ childMeta && meta && !hasRootRoute ? [childMeta, meta].flat() : childMeta || meta;
158
+
151
159
  /** @type {GeneratedRoutes} */
152
160
  let routeWithGroupFiles = {};
153
161
  if (typeof val === 'string') {
154
162
  routeWithGroupFiles = { '/': val };
155
- } else {
163
+ } else if (!Array.isArray(val)) {
156
164
  routeWithGroupFiles = { ...val };
157
165
  }
158
- if (layout) routeWithGroupFiles.layout = layout;
159
- if (hooks) routeWithGroupFiles.hooks = hooks;
160
- if (meta) routeWithGroupFiles.meta = meta;
166
+ if (layout) routeWithGroupFiles.layout = /** @type {string} */ (layout);
167
+ if (hooks) routeWithGroupFiles.hooks = /** @type {string} */ (hooks);
168
+ if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
161
169
  if (result[key]) {
162
170
  throw new Error(`Route conflict at \`${key}\``);
163
171
  }
@@ -184,12 +192,18 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
184
192
  /** @type {GeneratedRoutes} */
185
193
  const result = {};
186
194
  for (const [key, value] of Object.entries(routes)) {
187
- if (typeof value === 'object') {
195
+ if (typeof value === 'object' && !Array.isArray(value)) {
188
196
  result[key] = handleImports(value, routesPath);
197
+ } else if (key === 'meta' && Array.isArray(value)) {
198
+ const varNames = value.map((metaPath) => {
199
+ const variableName = pathToCorrectCasing(metaPath);
200
+ importsMap.set(variableName, routesPath + metaPath);
201
+ return variableName;
202
+ });
203
+ result[key] = `{ ...${varNames.toReversed().join(', ...')} }`;
189
204
  } else if (
190
- key === 'hooks' ||
191
- key === 'meta' ||
192
- (!value.endsWith('.lazy.svelte') && !allLazy)
205
+ typeof value === 'string' &&
206
+ (key === 'hooks' || key === 'meta' || (!value.endsWith('.lazy.svelte') && !allLazy))
193
207
  ) {
194
208
  const variableName = pathToCorrectCasing(value);
195
209
  importsMap.set(variableName, routesPath + value);
@@ -35,6 +35,7 @@ export function writeRouterCode() {
35
35
  const routerCode = generateRouterCode(genConfig.routesPath, {
36
36
  allLazy: genConfig.allLazy,
37
37
  js: genConfig.routesInJs,
38
+ ignore: genConfig.ignore,
38
39
  });
39
40
  const written = writeFileIfDifferent(genConfig.routerPath, routerCode);
40
41
 
@@ -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) {
@@ -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;
@@ -19,6 +19,12 @@ export type RouterOptions = {
19
19
  * @default 'src/routes'
20
20
  */
21
21
  path?: string;
22
+ /**
23
+ * Regular expressions for files to ignore when generating routes.
24
+ *
25
+ * @default empty array
26
+ */
27
+ ignore?: RegExp[];
22
28
  };
23
29
 
24
30
  export const router: (options?: RouterOptions) => Plugin;
@@ -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.ignore = options?.ignore || [];
11
12
  genConfig.routesInJs = options?.js || false;
12
13
  genConfig.routesPath = options?.path || 'src/routes';
13
14