sv-router 0.0.1 → 0.0.3
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 +69 -66
- package/{dist → src}/RecursiveComponentTree.svelte +4 -4
- package/{dist → src}/Router.svelte +2 -2
- package/src/cli/index.js +5 -0
- package/src/create-router.svelte.js +67 -0
- package/src/gen/config.js +16 -0
- package/src/gen/generate-router-code.js +102 -0
- package/src/gen/write-router-code.js +52 -0
- package/src/helpers/match-route.js +112 -0
- package/src/helpers/utils.js +46 -0
- package/src/helpers/validate-routes.js +46 -0
- package/src/index.d.ts +77 -0
- package/src/index.js +2 -0
- package/src/vite-plugin/index.d.ts +12 -0
- package/src/vite-plugin/index.js +1 -0
- package/src/vite-plugin/plugin.js +42 -0
- package/dist/RecursiveComponentTree.svelte.d.ts +0 -8
- package/dist/helpers/construct-path.d.ts +0 -3
- package/dist/helpers/construct-path.js +0 -10
- package/dist/helpers/match-route.d.ts +0 -7
- package/dist/helpers/match-route.js +0 -65
- package/dist/helpers/match-route.test.d.ts +0 -1
- package/dist/helpers/match-route.test.js +0 -133
- package/dist/helpers/utils.d.ts +0 -5
- package/dist/helpers/utils.js +0 -18
- package/dist/helpers/validate-routes.d.ts +0 -3
- package/dist/helpers/validate-routes.js +0 -34
- package/dist/helpers/validate-routes.test.d.ts +0 -1
- package/dist/helpers/validate-routes.test.js +0 -67
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/router.svelte.d.ts +0 -13
- package/dist/router.svelte.js +0 -50
- package/dist/types/test-types.d.ts +0 -1
- package/dist/types/test-types.js +0 -1
- package/dist/types/types.d.ts +0 -26
- package/dist/types/types.js +0 -1
package/package.json
CHANGED
|
@@ -1,67 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
2
|
+
"name": "sv-router",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Modern Svelte routing",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"svelte",
|
|
7
|
+
"router",
|
|
8
|
+
"spa"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/colinlienard/sv-router",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/colinlienard/sv-router/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/colinlienard/sv-router.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./src/index.js",
|
|
23
|
+
"svelte": "./src/index.js",
|
|
24
|
+
"types": "./src/index.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./vite-plugin": {
|
|
27
|
+
"import": "./src/vite-plugin/index.js",
|
|
28
|
+
"types": "./src/vite-plugin/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"bin": "./src/cli/index.js",
|
|
32
|
+
"files": [
|
|
33
|
+
"src"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"esm-env": "^1.2.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@changesets/cli": "^2.27.10",
|
|
40
|
+
"@eslint/js": "^9.16.0",
|
|
41
|
+
"@types/node": "^22.10.1",
|
|
42
|
+
"eslint-config-prettier": "^9.1.0",
|
|
43
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
44
|
+
"eslint-plugin-svelte": "^2.46.1",
|
|
45
|
+
"eslint-plugin-unicorn": "^56.0.1",
|
|
46
|
+
"globals": "^15.13.0",
|
|
47
|
+
"prettier": "^3.4.2",
|
|
48
|
+
"prettier-plugin-jsdoc": "^1.3.0",
|
|
49
|
+
"prettier-plugin-svelte": "^3.3.2",
|
|
50
|
+
"type-testing": "^0.2.0",
|
|
51
|
+
"typescript": "^5.7.2",
|
|
52
|
+
"typescript-eslint": "^8.18.0",
|
|
53
|
+
"vite": "^6.0.3",
|
|
54
|
+
"vitest": "^2.1.8"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"svelte": "^5"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"ex:basic": "pnpm --filter basic-example",
|
|
61
|
+
"ex:file-based": "pnpm --filter file-based-example",
|
|
62
|
+
"test": "vitest",
|
|
63
|
+
"check": "tsc --noEmit && pnpm -r check",
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"lint:fix": "eslint . --fix",
|
|
66
|
+
"format": "prettier . --check",
|
|
67
|
+
"format:fix": "prettier . --write",
|
|
68
|
+
"changeset": "changeset"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import
|
|
1
|
+
<script>
|
|
2
|
+
import { paramsStore } from './create-router.svelte.js';
|
|
3
3
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
4
|
-
import { paramsStore } from './router.svelte.ts';
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
/** @type {{ tree: import('svelte').Component[] }} */
|
|
6
|
+
let { tree } = $props();
|
|
7
7
|
|
|
8
8
|
const FirstComponent = $derived(tree[0]);
|
|
9
9
|
const restTree = $derived(tree.slice(1));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script>
|
|
2
2
|
import { on } from 'svelte/events';
|
|
3
|
+
import { componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
|
|
3
4
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
4
|
-
import { componentTree, onGlobalClick, onNavigate } from './router.svelte.ts';
|
|
5
5
|
|
|
6
6
|
onNavigate();
|
|
7
7
|
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BROWSER, DEV } from 'esm-env';
|
|
2
|
+
import { matchRoute } from './helpers/match-route.js';
|
|
3
|
+
import { constructPath, resolveRouteComponents } from './helpers/utils.js';
|
|
4
|
+
|
|
5
|
+
/** @type {import('./index.d.ts').Routes} */
|
|
6
|
+
export let routes;
|
|
7
|
+
|
|
8
|
+
/** @type {import('svelte').Component[]} */
|
|
9
|
+
export const componentTree = $state([]);
|
|
10
|
+
|
|
11
|
+
/** @type {Record<string, string>} */
|
|
12
|
+
export const paramsStore = $state({});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @template {import('./index.d.ts').Routes} T
|
|
16
|
+
* @param {T} r
|
|
17
|
+
* @returns {import('./index.d.ts').RouterMethods<T>}
|
|
18
|
+
*/
|
|
19
|
+
export function createRouter(r) {
|
|
20
|
+
routes = r;
|
|
21
|
+
|
|
22
|
+
if (DEV && BROWSER) {
|
|
23
|
+
import('./helpers/validate-routes.js').then(({ validateRoutes }) => {
|
|
24
|
+
validateRoutes(routes);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
path: constructPath,
|
|
30
|
+
goto(...args) {
|
|
31
|
+
const path = constructPath(args[0], args[1]);
|
|
32
|
+
globalThis.history.pushState({}, '', path);
|
|
33
|
+
onNavigate();
|
|
34
|
+
},
|
|
35
|
+
params() {
|
|
36
|
+
const readonly = $derived(paramsStore);
|
|
37
|
+
return readonly;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function onNavigate() {
|
|
43
|
+
if (!routes) {
|
|
44
|
+
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
45
|
+
}
|
|
46
|
+
const { match, layouts, params } = matchRoute(globalThis.location.pathname, routes);
|
|
47
|
+
resolveRouteComponents(match ? [...layouts, match] : layouts).then((components) => {
|
|
48
|
+
Object.assign(componentTree, components);
|
|
49
|
+
});
|
|
50
|
+
Object.assign(paramsStore, params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {Event} event */
|
|
54
|
+
export function onGlobalClick(event) {
|
|
55
|
+
const anchor = /** @type {HTMLElement} */ (event.target).closest('a');
|
|
56
|
+
if (!anchor) return;
|
|
57
|
+
|
|
58
|
+
if (anchor.hasAttribute('target') || anchor.hasAttribute('download')) return;
|
|
59
|
+
|
|
60
|
+
const url = new URL(anchor.href);
|
|
61
|
+
const currentOrigin = globalThis.location.origin;
|
|
62
|
+
if (url.origin !== currentOrigin) return;
|
|
63
|
+
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
globalThis.history.pushState({}, '', anchor.href);
|
|
66
|
+
onNavigate();
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {{
|
|
3
|
+
* routesPath: string;
|
|
4
|
+
* readonly genCodeDirPath: string;
|
|
5
|
+
* readonly routerPath: string;
|
|
6
|
+
* readonly tsconfigPath: string;
|
|
7
|
+
* readonly genCodeAlias: string;
|
|
8
|
+
* }}
|
|
9
|
+
*/
|
|
10
|
+
export const genConfig = {
|
|
11
|
+
routesPath: 'src/routes',
|
|
12
|
+
genCodeDirPath: '.router',
|
|
13
|
+
routerPath: '.router/router.ts',
|
|
14
|
+
tsconfigPath: '.router/tsconfig.json',
|
|
15
|
+
genCodeAlias: 'sv-router/generated',
|
|
16
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** @typedef {(string | { name: string; tree: FileTree })[]} FileTree */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* [key: string]: string | GeneratedRoutes;
|
|
9
|
+
* }} GeneratedRoutes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PARAM_FILENAME_REGEX = /\[(.*)\].svelte/g;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} routesPath
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function generateRouterCode(routesPath) {
|
|
19
|
+
const fileTree = buildFileTree(path.join(process.cwd(), routesPath));
|
|
20
|
+
const routeMap = createRouteMap(fileTree);
|
|
21
|
+
return createRouterCode(routeMap, path.join('..', routesPath));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} routesPath
|
|
26
|
+
* @returns {FileTree}
|
|
27
|
+
*/
|
|
28
|
+
export function buildFileTree(routesPath) {
|
|
29
|
+
const entries = fs.readdirSync(routesPath);
|
|
30
|
+
/** @type {FileTree} */
|
|
31
|
+
const result = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const stat = fs.lstatSync(path.join(routesPath, entry));
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
result.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
|
|
36
|
+
} else if (entry.endsWith('.svelte')) {
|
|
37
|
+
result.push(entry);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {FileTree} fileTree
|
|
45
|
+
* @param {string} prefix
|
|
46
|
+
* @returns {GeneratedRoutes}
|
|
47
|
+
*/
|
|
48
|
+
export function createRouteMap(fileTree, prefix = '') {
|
|
49
|
+
/** @type {GeneratedRoutes} */
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const entry of fileTree) {
|
|
52
|
+
if (typeof entry === 'string') {
|
|
53
|
+
switch (entry) {
|
|
54
|
+
case 'index.svelte': {
|
|
55
|
+
result['/'] = prefix + entry;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case '*.svelte': {
|
|
59
|
+
result['*'] = prefix + entry;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case '_layout.svelte': {
|
|
63
|
+
result['layout'] = prefix + entry;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
default: {
|
|
67
|
+
if (PARAM_FILENAME_REGEX.test(entry)) {
|
|
68
|
+
result['/' + entry.replaceAll(PARAM_FILENAME_REGEX, ':$1')] = prefix + entry;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
result['/' + entry.replace('.svelte', '')] = prefix + entry;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
result['/' + entry.name] = createRouteMap(entry.tree, prefix + entry.name + '/');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {GeneratedRoutes} routes
|
|
84
|
+
* @param {string} routesPath
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function createRouterCode(routes, routesPath) {
|
|
88
|
+
if (!routesPath.endsWith('/')) {
|
|
89
|
+
routesPath += '/';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const jsonRoutes = JSON.stringify(routes, undefined, 2);
|
|
93
|
+
const withImports = jsonRoutes.replaceAll(
|
|
94
|
+
/"(.*)": "(.*)",?/g,
|
|
95
|
+
`"$1": () => import("${routesPath}$2"),`,
|
|
96
|
+
);
|
|
97
|
+
return [
|
|
98
|
+
'import { createRouter } from "sv-router";',
|
|
99
|
+
'\n\n',
|
|
100
|
+
`export const { path, goto, params } = createRouter(${withImports});`,
|
|
101
|
+
].join('');
|
|
102
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { genConfig } from './config.js';
|
|
5
|
+
import { generateRouterCode } from './generate-router-code.js';
|
|
6
|
+
|
|
7
|
+
export function writeRouterCode() {
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(genConfig.genCodeDirPath)) {
|
|
10
|
+
fs.mkdirSync(genConfig.genCodeDirPath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Write `.router/router.ts` file
|
|
14
|
+
const routerCode = generateRouterCode(genConfig.routesPath);
|
|
15
|
+
writeFileIfDifferent(genConfig.routerPath, routerCode);
|
|
16
|
+
|
|
17
|
+
// Write `.router/tsconfig.json` file
|
|
18
|
+
const tsConfig = {
|
|
19
|
+
compilerOptions: {
|
|
20
|
+
module: 'preserve',
|
|
21
|
+
moduleResolution: 'bundler',
|
|
22
|
+
paths: {
|
|
23
|
+
[genConfig.genCodeAlias]: [path.join('..', genConfig.routerPath)],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
include: [
|
|
27
|
+
'../src/**/*.js',
|
|
28
|
+
'../src/**/*.ts',
|
|
29
|
+
'../src/**/*.svelte',
|
|
30
|
+
'../tests/**/*.js',
|
|
31
|
+
'../tests/**/*.ts',
|
|
32
|
+
'../tests/**/*.svelte',
|
|
33
|
+
'./router.ts',
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
writeFileIfDifferent(genConfig.tsconfigPath, JSON.stringify(tsConfig, undefined, 2));
|
|
37
|
+
|
|
38
|
+
console.log('✅️ Routes generated');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error during routes generation:', error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @param {string} content
|
|
47
|
+
*/
|
|
48
|
+
function writeFileIfDifferent(filePath, content) {
|
|
49
|
+
if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
|
|
50
|
+
fs.writeFileSync(filePath, content);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../index.d.ts').LayoutComponent} LayoutComponent
|
|
3
|
+
*
|
|
4
|
+
* @typedef {import('../index.d.ts').RouteComponent} RouteComponent
|
|
5
|
+
*
|
|
6
|
+
* @typedef {import('../index.d.ts').Routes} Routes
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} pathname
|
|
11
|
+
* @param {Routes} routes
|
|
12
|
+
* @returns {{
|
|
13
|
+
* match: RouteComponent | undefined;
|
|
14
|
+
* layouts: LayoutComponent[];
|
|
15
|
+
* params: Record<string, string>;
|
|
16
|
+
* breakFromLayouts: boolean;
|
|
17
|
+
* }}
|
|
18
|
+
*/
|
|
19
|
+
export function matchRoute(pathname, routes) {
|
|
20
|
+
// Remove trailing slash
|
|
21
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
22
|
+
pathname = pathname.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
const pathParts = pathname.split('/');
|
|
25
|
+
const allRouteParts = sortRoutes(Object.keys(routes)).map((route) => route.split('/'));
|
|
26
|
+
|
|
27
|
+
/** @type {RouteComponent | undefined} */
|
|
28
|
+
let match;
|
|
29
|
+
|
|
30
|
+
/** @type {LayoutComponent[]} */
|
|
31
|
+
let layouts = [];
|
|
32
|
+
|
|
33
|
+
/** @type {Record<string, string>} */
|
|
34
|
+
let params = {};
|
|
35
|
+
|
|
36
|
+
let breakFromLayouts = false;
|
|
37
|
+
|
|
38
|
+
outer: for (const routeParts of allRouteParts) {
|
|
39
|
+
for (let [index, routePart] of sortRoutes(routeParts).entries()) {
|
|
40
|
+
breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
|
|
41
|
+
if (breakFromLayouts) {
|
|
42
|
+
routePart = routePart.slice(1, -1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pathPart = pathParts[index];
|
|
46
|
+
if (routePart.startsWith(':')) {
|
|
47
|
+
params[routePart.slice(1)] = pathPart;
|
|
48
|
+
} else if (routePart === '*') {
|
|
49
|
+
match = /** @type {RouteComponent} */ (
|
|
50
|
+
routes[/** @type {keyof Routes} */ (routeParts.join('/'))]
|
|
51
|
+
);
|
|
52
|
+
break outer;
|
|
53
|
+
} else if (routePart !== pathPart) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (index !== routeParts.length - 1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!breakFromLayouts && 'layout' in routes && routes.layout) {
|
|
62
|
+
layouts.push(routes.layout);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const routeMatch = /** @type {RouteComponent} */ (
|
|
66
|
+
routes[/** @type {keyof Routes} */ (routeParts.join('/'))]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (typeof routeMatch === 'function') {
|
|
70
|
+
if (routeParts.length === pathParts.length) {
|
|
71
|
+
match = routeMatch;
|
|
72
|
+
} else {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
} else if (routeMatch) {
|
|
76
|
+
const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
|
|
77
|
+
const result = matchRoute(nestedPathname, routeMatch);
|
|
78
|
+
if (result) {
|
|
79
|
+
match = result.match;
|
|
80
|
+
params = { ...params, ...result.params };
|
|
81
|
+
if (result.breakFromLayouts) {
|
|
82
|
+
layouts = [];
|
|
83
|
+
} else {
|
|
84
|
+
layouts.push(...result.layouts);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
break outer;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { match, layouts, params, breakFromLayouts };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string[]} routes
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
export function sortRoutes(routes) {
|
|
100
|
+
return routes.toSorted((a, b) => getRoutePriority(a) - getRoutePriority(b));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} route
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
function getRoutePriority(route) {
|
|
108
|
+
if (route === '' || route === '/') return 1;
|
|
109
|
+
if (route === '*') return 4;
|
|
110
|
+
if (route.includes(':')) return 3;
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} path
|
|
3
|
+
* @param {Record<string, string>} [params]
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function constructPath(path, params) {
|
|
7
|
+
if (!params) return path;
|
|
8
|
+
|
|
9
|
+
let result = path;
|
|
10
|
+
for (const key in params) {
|
|
11
|
+
result = result.replace(`:${key}`, params[key]);
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('../index.d.ts').RouteComponent[]} input
|
|
18
|
+
* @returns {Promise<import('svelte').Component[]>}
|
|
19
|
+
*/
|
|
20
|
+
export function resolveRouteComponents(input) {
|
|
21
|
+
return Promise.all(input.map((c) => resolveRouteComponent(c)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('../index.d.ts').RouteComponent} input
|
|
26
|
+
* @returns {Promise<import('svelte').Component>}
|
|
27
|
+
*/
|
|
28
|
+
export function resolveRouteComponent(input) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
if (isLazyImport(input)) {
|
|
31
|
+
Promise.resolve(input()).then((module) => {
|
|
32
|
+
resolve(module.default);
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
resolve(input);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {unknown} input
|
|
42
|
+
* @returns {input is import('../index.d.ts').LazyRouteComponent}
|
|
43
|
+
*/
|
|
44
|
+
export function isLazyImport(input) {
|
|
45
|
+
return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/g.test(String(input));
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @param {import('../index.d.ts').Routes} routes */
|
|
2
|
+
export function validateRoutes(routes) {
|
|
3
|
+
const paths = getRoutePaths(routes);
|
|
4
|
+
const wildcardPaths = paths.filter((path) => path.endsWith('*'));
|
|
5
|
+
for (const wildcardPath of wildcardPaths) {
|
|
6
|
+
const parentPath = wildcardPath.slice(0, -1);
|
|
7
|
+
const dynamicPath = paths.find(
|
|
8
|
+
(p) =>
|
|
9
|
+
p !== '/' &&
|
|
10
|
+
!p.endsWith('*') &&
|
|
11
|
+
p.startsWith(parentPath === '' ? '/:' : parentPath) &&
|
|
12
|
+
p.match(/:[^/]*$/g), // Match dynamic paths without slashes after the colon
|
|
13
|
+
);
|
|
14
|
+
if (dynamicPath) {
|
|
15
|
+
console.warn(
|
|
16
|
+
`Router warning: Wildcard route \`${wildcardPath}\` should not be at the same level as dynamic route \`${dynamicPath}\`.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('../index.d.ts').Routes} routes
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
export function getRoutePaths(routes) {
|
|
27
|
+
const paths = [];
|
|
28
|
+
for (const [key, value] of Object.entries(routes)) {
|
|
29
|
+
if (typeof value === 'object') {
|
|
30
|
+
paths.push(
|
|
31
|
+
...getRoutePaths(value).map((path) => {
|
|
32
|
+
if (path === '*') {
|
|
33
|
+
return key + '/*';
|
|
34
|
+
}
|
|
35
|
+
if (path === '/') {
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
return key + path;
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
paths.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return paths;
|
|
46
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Setup a new router instance with the given routes.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* export const { path, goto, params } = createRouter({
|
|
8
|
+
* '/': Home,
|
|
9
|
+
* '/about': About,
|
|
10
|
+
* ...
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function createRouter<T extends Routes>(r: T): RouterMethods<T>;
|
|
15
|
+
export const Router: Component;
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
18
|
+
type BaseProps = {};
|
|
19
|
+
|
|
20
|
+
export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
|
|
21
|
+
default: Component<Props>;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type RouteComponent<Props extends BaseProps = any> =
|
|
25
|
+
| Component<Props>
|
|
26
|
+
| LazyRouteComponent<Props>;
|
|
27
|
+
export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
28
|
+
|
|
29
|
+
export type Routes = {
|
|
30
|
+
[_: `/${string}`]: RouteComponent | Routes;
|
|
31
|
+
'*'?: RouteComponent;
|
|
32
|
+
layout?: LayoutComponent;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type RouterMethods<T extends Routes> = {
|
|
36
|
+
path<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
|
|
37
|
+
goto<U extends Path<T>>(...args: ConstructPathArgs<U>): void;
|
|
38
|
+
params(): AllParams<T>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type Path<T extends Routes> = RemoveParenthesis<
|
|
42
|
+
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>>>
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export type ConstructPathArgs<T extends string> =
|
|
46
|
+
PathParams<T> extends never ? [T] : [T, PathParams<T>];
|
|
47
|
+
|
|
48
|
+
export type PathParams<T extends string> =
|
|
49
|
+
ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
|
|
50
|
+
|
|
51
|
+
export type AllParams<T extends Routes> = Partial<Record<ExtractParams<RecursiveKeys<T>>, string>>;
|
|
52
|
+
|
|
53
|
+
type StripNonRoutes<T extends Routes> = {
|
|
54
|
+
[K in keyof T as K extends '*' ? never : K extends 'layout' ? never : K]: T[K] extends Routes
|
|
55
|
+
? StripNonRoutes<T[K]>
|
|
56
|
+
: T[K];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|
|
60
|
+
[K in keyof T]: K extends string
|
|
61
|
+
? T[K] extends Routes
|
|
62
|
+
? RecursiveKeys<T[K], `${Prefix}${K}`>
|
|
63
|
+
: `${Prefix}${K}`
|
|
64
|
+
: never;
|
|
65
|
+
}[keyof T];
|
|
66
|
+
|
|
67
|
+
type RemoveLastSlash<T extends string> = T extends '/' ? T : T extends `${infer R}/` ? R : T;
|
|
68
|
+
|
|
69
|
+
type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${infer C}`
|
|
70
|
+
? RemoveParenthesis<`${A}${B}${C}`>
|
|
71
|
+
: T;
|
|
72
|
+
|
|
73
|
+
type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
|
|
74
|
+
? Param | ExtractParams<`/${Rest}`>
|
|
75
|
+
: T extends `${string}:${infer Param}`
|
|
76
|
+
? Param
|
|
77
|
+
: never;
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { router } from './plugin.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { genConfig } from '../gen/config.js';
|
|
3
|
+
import { writeRouterCode } from '../gen/write-router-code.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('./index.d.ts').RouterOptions | undefined} options
|
|
7
|
+
* @returns {import('vite').Plugin}
|
|
8
|
+
*/
|
|
9
|
+
export function router(options) {
|
|
10
|
+
if (options?.path) {
|
|
11
|
+
genConfig.routesPath = options.path;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'sv-router',
|
|
16
|
+
config(config) {
|
|
17
|
+
if (!config.resolve) {
|
|
18
|
+
config.resolve = {};
|
|
19
|
+
}
|
|
20
|
+
if (!config.resolve.alias) {
|
|
21
|
+
config.resolve.alias = {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const replacement = path.resolve(process.cwd(), genConfig.routerPath);
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(config.resolve.alias)) {
|
|
27
|
+
config.resolve.alias.push({ find: genConfig.genCodeAlias, replacement });
|
|
28
|
+
} else {
|
|
29
|
+
/** @type {Record<string, string>} */ (config.resolve.alias)[genConfig.genCodeAlias] =
|
|
30
|
+
replacement;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
buildStart() {
|
|
34
|
+
writeRouterCode();
|
|
35
|
+
},
|
|
36
|
+
watchChange(file) {
|
|
37
|
+
if (file.includes(genConfig.routesPath)) {
|
|
38
|
+
writeRouterCode();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Component } from 'svelte';
|
|
2
|
-
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
3
|
-
type $$ComponentProps = {
|
|
4
|
-
tree: Component[];
|
|
5
|
-
};
|
|
6
|
-
declare const RecursiveComponentTree: Component<$$ComponentProps, {}, "">;
|
|
7
|
-
type RecursiveComponentTree = ReturnType<typeof RecursiveComponentTree>;
|
|
8
|
-
export default RecursiveComponentTree;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { LayoutComponent, RouteComponent, Routes } from '../types/types.ts';
|
|
2
|
-
export declare function matchRoute(pathname: string, routes: Routes): {
|
|
3
|
-
match: RouteComponent | undefined;
|
|
4
|
-
layouts: LayoutComponent[];
|
|
5
|
-
params: Record<string, string>;
|
|
6
|
-
};
|
|
7
|
-
export declare function sortRoutes(routes: string[]): string[];
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
export function matchRoute(pathname, routes) {
|
|
2
|
-
// Remove trailing slash
|
|
3
|
-
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
4
|
-
pathname = pathname.slice(0, -1);
|
|
5
|
-
}
|
|
6
|
-
const pathParts = pathname.split('/');
|
|
7
|
-
const allRouteParts = sortRoutes(Object.keys(routes)).map((route) => route.split('/'));
|
|
8
|
-
let match;
|
|
9
|
-
const layouts = [];
|
|
10
|
-
let params = {};
|
|
11
|
-
outer: for (const routeParts of allRouteParts) {
|
|
12
|
-
for (const [index, routePart] of sortRoutes(routeParts).entries()) {
|
|
13
|
-
const pathPart = pathParts[index];
|
|
14
|
-
if (routePart.startsWith(':')) {
|
|
15
|
-
params[routePart.slice(1)] = pathPart;
|
|
16
|
-
}
|
|
17
|
-
else if (routePart === '*') {
|
|
18
|
-
match = routes[routeParts.join('/')];
|
|
19
|
-
break outer;
|
|
20
|
-
}
|
|
21
|
-
else if (routePart !== pathPart) {
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
const routeMatch = routes[routeParts.join('/')];
|
|
25
|
-
if (typeof routeMatch !== 'function' &&
|
|
26
|
-
routeMatch?.layout &&
|
|
27
|
-
!layouts.includes(routeMatch.layout)) {
|
|
28
|
-
layouts.push(routeMatch.layout);
|
|
29
|
-
}
|
|
30
|
-
if (index === routeParts.length - 1) {
|
|
31
|
-
if (typeof routeMatch === 'function') {
|
|
32
|
-
if (routeParts.length === pathParts.length) {
|
|
33
|
-
match = routeMatch;
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
else if (routeMatch) {
|
|
40
|
-
const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
|
|
41
|
-
const result = matchRoute(nestedPathname, routeMatch);
|
|
42
|
-
if (result) {
|
|
43
|
-
match = result.match;
|
|
44
|
-
params = { ...params, ...result.params };
|
|
45
|
-
layouts.push(...result.layouts);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
break outer;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return { match, layouts, params };
|
|
53
|
-
}
|
|
54
|
-
export function sortRoutes(routes) {
|
|
55
|
-
return routes.toSorted((a, b) => getRoutePriority(a) - getRoutePriority(b));
|
|
56
|
-
}
|
|
57
|
-
function getRoutePriority(route) {
|
|
58
|
-
if (route === '' || route === '/')
|
|
59
|
-
return 1;
|
|
60
|
-
if (route === '*')
|
|
61
|
-
return 4;
|
|
62
|
-
if (route.includes(':'))
|
|
63
|
-
return 3;
|
|
64
|
-
return 2;
|
|
65
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { matchRoute, sortRoutes } from "./match-route.js";
|
|
2
|
-
const Home = (() => 'Home');
|
|
3
|
-
const Posts = (() => 'Posts');
|
|
4
|
-
const StaticPost = (() => 'StaticPost');
|
|
5
|
-
const DynamicPost = (() => 'DynamicPost');
|
|
6
|
-
const DynamicPostComment = (() => 'DynamicPostComment');
|
|
7
|
-
const UserNotFound = (() => 'UserNotFound');
|
|
8
|
-
const PageNotFound = (() => 'PageNotFound');
|
|
9
|
-
const Layout1 = (() => 'Layout1');
|
|
10
|
-
const Layout2 = (() => 'Layout2');
|
|
11
|
-
describe('matchRoute', () => {
|
|
12
|
-
describe.each([
|
|
13
|
-
{
|
|
14
|
-
mode: 'flat',
|
|
15
|
-
routes: {
|
|
16
|
-
'/': Home,
|
|
17
|
-
'/posts': Posts,
|
|
18
|
-
'/posts/static': StaticPost,
|
|
19
|
-
'/posts/:id': DynamicPost,
|
|
20
|
-
'/posts/:id/:commentId': DynamicPostComment,
|
|
21
|
-
'/users/*': UserNotFound,
|
|
22
|
-
'*': PageNotFound,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
mode: 'flat unordered',
|
|
27
|
-
routes: {
|
|
28
|
-
'/posts/:id/:commentId': DynamicPostComment,
|
|
29
|
-
'/posts': Posts,
|
|
30
|
-
'/users/*': UserNotFound,
|
|
31
|
-
'/posts/static': StaticPost,
|
|
32
|
-
'*': PageNotFound,
|
|
33
|
-
'/posts/:id': DynamicPost,
|
|
34
|
-
'/': Home,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
mode: 'tree',
|
|
39
|
-
routes: {
|
|
40
|
-
'/': Home,
|
|
41
|
-
'/posts': {
|
|
42
|
-
'/': Posts,
|
|
43
|
-
'/static': StaticPost,
|
|
44
|
-
'/:id': {
|
|
45
|
-
'/': DynamicPost,
|
|
46
|
-
'/:commentId': DynamicPostComment,
|
|
47
|
-
layout: Layout2,
|
|
48
|
-
},
|
|
49
|
-
layout: Layout1,
|
|
50
|
-
},
|
|
51
|
-
'/users/*': UserNotFound,
|
|
52
|
-
'*': PageNotFound,
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
mode: 'tree unordered',
|
|
57
|
-
routes: {
|
|
58
|
-
'*': PageNotFound,
|
|
59
|
-
'/posts': {
|
|
60
|
-
'/:id': {
|
|
61
|
-
'/:commentId': DynamicPostComment,
|
|
62
|
-
'/': DynamicPost,
|
|
63
|
-
layout: Layout2,
|
|
64
|
-
},
|
|
65
|
-
'/': Posts,
|
|
66
|
-
'/static': StaticPost,
|
|
67
|
-
layout: Layout1,
|
|
68
|
-
},
|
|
69
|
-
'/users/*': UserNotFound,
|
|
70
|
-
'/': Home,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
])('$mode paths', ({ mode, routes: r }) => {
|
|
74
|
-
const routes = r;
|
|
75
|
-
it('should match the root route', () => {
|
|
76
|
-
const { match } = matchRoute('/', routes);
|
|
77
|
-
expect(match).toEqual(Home);
|
|
78
|
-
});
|
|
79
|
-
it('should match a simple route', () => {
|
|
80
|
-
const { match } = matchRoute('/posts', routes);
|
|
81
|
-
expect(match).toEqual(Posts);
|
|
82
|
-
});
|
|
83
|
-
it('should match a simple route with a trailing slash', () => {
|
|
84
|
-
const { match } = matchRoute('/posts', routes);
|
|
85
|
-
expect(match).toEqual(Posts);
|
|
86
|
-
});
|
|
87
|
-
it('should match a nested route', () => {
|
|
88
|
-
const { match } = matchRoute('/posts/static', routes);
|
|
89
|
-
expect(match).toEqual(StaticPost);
|
|
90
|
-
});
|
|
91
|
-
it('should match a dynamic route and return a param', () => {
|
|
92
|
-
const { match, params } = matchRoute('/posts/bar', routes);
|
|
93
|
-
expect(match).toEqual(DynamicPost);
|
|
94
|
-
expect(params).toEqual({ id: 'bar' });
|
|
95
|
-
});
|
|
96
|
-
it('should match multiple dynamic nested routes and return params', () => {
|
|
97
|
-
const { match, params } = matchRoute('/posts/bar/baz', routes);
|
|
98
|
-
expect(match).toEqual(DynamicPostComment);
|
|
99
|
-
expect(params).toEqual({ id: 'bar', commentId: 'baz' });
|
|
100
|
-
});
|
|
101
|
-
if (mode === 'nested') {
|
|
102
|
-
it('should match routes with layout', () => {
|
|
103
|
-
const { layouts: layouts1 } = matchRoute('/', routes);
|
|
104
|
-
const { layouts: layouts2 } = matchRoute('/posts', routes);
|
|
105
|
-
const { layouts: layouts3 } = matchRoute('/posts/static', routes);
|
|
106
|
-
const { layouts: layouts4 } = matchRoute('/posts/bar/baz', routes);
|
|
107
|
-
expect(layouts1).toEqual([]);
|
|
108
|
-
expect(layouts2).toEqual([Layout1]);
|
|
109
|
-
expect(layouts3).toEqual([Layout1]);
|
|
110
|
-
expect(layouts4).toEqual([Layout1, Layout2]);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
it('should match wildcard nested route', () => {
|
|
114
|
-
const { match } = matchRoute('/users/notfound', routes);
|
|
115
|
-
expect(match).toEqual(UserNotFound);
|
|
116
|
-
});
|
|
117
|
-
it('should match wildcard route', () => {
|
|
118
|
-
const { match } = matchRoute('/notfound', routes);
|
|
119
|
-
expect(match).toEqual(PageNotFound);
|
|
120
|
-
});
|
|
121
|
-
it('should not match any route', () => {
|
|
122
|
-
delete routes['*'];
|
|
123
|
-
const { match } = matchRoute('/notfound', routes);
|
|
124
|
-
expect(match).toBeUndefined();
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe('sortRoutes', () => {
|
|
129
|
-
it('should sort routes', () => {
|
|
130
|
-
const result = sortRoutes(['/:id', '*', '/foo', '', '/']);
|
|
131
|
-
expect(result).toEqual(['', '/', '/foo', '/:id', '*']);
|
|
132
|
-
});
|
|
133
|
-
});
|
package/dist/helpers/utils.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { Component } from 'svelte';
|
|
2
|
-
import type { LazyRouteComponent, RouteComponent } from '../types/types.ts';
|
|
3
|
-
export declare function resolveRouteComponents(input: RouteComponent<any>[]): Promise<Component[]>;
|
|
4
|
-
export declare function resolveRouteComponent(input: RouteComponent): Promise<Component>;
|
|
5
|
-
export declare function isLazyImport(input: unknown): input is LazyRouteComponent;
|
package/dist/helpers/utils.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function resolveRouteComponents(input) {
|
|
2
|
-
return Promise.all(input.map((c) => resolveRouteComponent(c)));
|
|
3
|
-
}
|
|
4
|
-
export function resolveRouteComponent(input) {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
if (isLazyImport(input)) {
|
|
7
|
-
Promise.resolve(input()).then((module) => {
|
|
8
|
-
resolve(module.default);
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
else {
|
|
12
|
-
resolve(input);
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
export function isLazyImport(input) {
|
|
17
|
-
return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/g.test(String(input));
|
|
18
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export function validateRoutes(routes) {
|
|
2
|
-
const paths = getRoutePaths(routes);
|
|
3
|
-
const wildcardPaths = paths.filter((path) => path.endsWith('*'));
|
|
4
|
-
for (const wildcardPath of wildcardPaths) {
|
|
5
|
-
const parentPath = wildcardPath.slice(0, -1);
|
|
6
|
-
const dynamicPath = paths.find((p) => p !== '/' &&
|
|
7
|
-
!p.endsWith('*') &&
|
|
8
|
-
p.startsWith(parentPath === '' ? '/:' : parentPath) &&
|
|
9
|
-
p.match(/:[^/]*$/g));
|
|
10
|
-
if (dynamicPath) {
|
|
11
|
-
console.warn(`Router warning: Wildcard route \`${wildcardPath}\` should not be at the same level as dynamic route \`${dynamicPath}\`.`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export function getRoutePaths(routes) {
|
|
16
|
-
const paths = [];
|
|
17
|
-
for (const [key, value] of Object.entries(routes)) {
|
|
18
|
-
if (typeof value === 'object') {
|
|
19
|
-
paths.push(...getRoutePaths(value).map((path) => {
|
|
20
|
-
if (path === '*') {
|
|
21
|
-
return key + '/*';
|
|
22
|
-
}
|
|
23
|
-
if (path === '/') {
|
|
24
|
-
return key;
|
|
25
|
-
}
|
|
26
|
-
return key + path;
|
|
27
|
-
}));
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
paths.push(key);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return paths;
|
|
34
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { getRoutePaths, validateRoutes } from "./validate-routes.js";
|
|
2
|
-
const component = (() => ({}));
|
|
3
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn());
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
vi.clearAllMocks();
|
|
6
|
-
});
|
|
7
|
-
describe('validateRoutes', () => {
|
|
8
|
-
it('should validate routes', () => {
|
|
9
|
-
validateRoutes({
|
|
10
|
-
'/': component,
|
|
11
|
-
'/about': component,
|
|
12
|
-
'/posts': {
|
|
13
|
-
'/': component,
|
|
14
|
-
'/:id': component,
|
|
15
|
-
},
|
|
16
|
-
'*': component,
|
|
17
|
-
});
|
|
18
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
19
|
-
});
|
|
20
|
-
it('should raise a warning if a wildcard route is at the same level as a dynamic route', () => {
|
|
21
|
-
validateRoutes({
|
|
22
|
-
'/': component,
|
|
23
|
-
'/:id': component,
|
|
24
|
-
'*': component,
|
|
25
|
-
});
|
|
26
|
-
expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `*` should not be at the same level as dynamic route `/:id`.');
|
|
27
|
-
});
|
|
28
|
-
it.each([
|
|
29
|
-
{
|
|
30
|
-
'/': component,
|
|
31
|
-
'/posts/*': component,
|
|
32
|
-
'/posts/:id': component,
|
|
33
|
-
'/:id': component,
|
|
34
|
-
'*': component,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
'/': component,
|
|
38
|
-
'/posts': {
|
|
39
|
-
'*': component,
|
|
40
|
-
'/:id': component,
|
|
41
|
-
},
|
|
42
|
-
'/:id': component,
|
|
43
|
-
'*': component,
|
|
44
|
-
},
|
|
45
|
-
])('should raise multiple warnings if wildcard routes are at the same level as dynamic routes', (routes) => {
|
|
46
|
-
validateRoutes(routes);
|
|
47
|
-
expect(consoleSpy).toHaveBeenCalledTimes(2);
|
|
48
|
-
expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `*` should not be at the same level as dynamic route `/:id`.');
|
|
49
|
-
expect(consoleSpy).toHaveBeenCalledWith('Router warning: Wildcard route `/posts/*` should not be at the same level as dynamic route `/posts/:id`.');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe('getRoutePaths', () => {
|
|
53
|
-
it('should return all paths in an array', () => {
|
|
54
|
-
const result = getRoutePaths({
|
|
55
|
-
'/': component,
|
|
56
|
-
'/about': component,
|
|
57
|
-
'/posts': {
|
|
58
|
-
'/': component,
|
|
59
|
-
'/:id': component,
|
|
60
|
-
'*': component,
|
|
61
|
-
},
|
|
62
|
-
'/foo/bar': component,
|
|
63
|
-
'*': component,
|
|
64
|
-
});
|
|
65
|
-
expect(result).toEqual(['/', '/about', '/posts', '/posts/:id', '/posts/*', '/foo/bar', '*']);
|
|
66
|
-
});
|
|
67
|
-
});
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
package/dist/router.svelte.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Component } from 'svelte';
|
|
2
|
-
import { type ConstructPathArgs } from './helpers/construct-path.ts';
|
|
3
|
-
import type { AllParams, Path, Routes } from './types/types.ts';
|
|
4
|
-
export declare let routes: Routes;
|
|
5
|
-
export declare const componentTree: Component<{}, {}, string>[];
|
|
6
|
-
export declare const paramsStore: Record<string, string>;
|
|
7
|
-
export declare function createRouter<T extends Routes>(r: T): {
|
|
8
|
-
path<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
|
|
9
|
-
goto<U extends Path<T>>(...args: ConstructPathArgs<U>): void;
|
|
10
|
-
params(): AllParams<T>;
|
|
11
|
-
};
|
|
12
|
-
export declare function onNavigate(): void;
|
|
13
|
-
export declare function onGlobalClick(event: Event): void;
|
package/dist/router.svelte.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { BROWSER, DEV } from 'esm-env';
|
|
2
|
-
import { constructPath } from "./helpers/construct-path.js";
|
|
3
|
-
import { matchRoute } from "./helpers/match-route.js";
|
|
4
|
-
import { resolveRouteComponents } from "./helpers/utils.js";
|
|
5
|
-
export let routes;
|
|
6
|
-
export const componentTree = $state([]);
|
|
7
|
-
export const paramsStore = $state({});
|
|
8
|
-
export function createRouter(r) {
|
|
9
|
-
routes = r;
|
|
10
|
-
if (DEV && BROWSER) {
|
|
11
|
-
import('./helpers/validate-routes.ts').then(({ validateRoutes }) => {
|
|
12
|
-
validateRoutes(routes);
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
path(...args) {
|
|
17
|
-
return constructPath(...args);
|
|
18
|
-
},
|
|
19
|
-
goto(...args) {
|
|
20
|
-
const path = constructPath(...args);
|
|
21
|
-
globalThis.history.pushState({}, '', path);
|
|
22
|
-
onNavigate();
|
|
23
|
-
},
|
|
24
|
-
params() {
|
|
25
|
-
const readonly = $derived(paramsStore);
|
|
26
|
-
return readonly;
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export function onNavigate() {
|
|
31
|
-
const { match, layouts, params } = matchRoute(globalThis.location.pathname, routes);
|
|
32
|
-
resolveRouteComponents(match ? [...layouts, match] : layouts).then((components) => {
|
|
33
|
-
Object.assign(componentTree, components);
|
|
34
|
-
});
|
|
35
|
-
Object.assign(paramsStore, params);
|
|
36
|
-
}
|
|
37
|
-
export function onGlobalClick(event) {
|
|
38
|
-
const anchor = event.target.closest('a');
|
|
39
|
-
if (!anchor)
|
|
40
|
-
return;
|
|
41
|
-
if (anchor.hasAttribute('target') || anchor.hasAttribute('download'))
|
|
42
|
-
return;
|
|
43
|
-
const url = new URL(anchor.href);
|
|
44
|
-
const currentOrigin = globalThis.location.origin;
|
|
45
|
-
if (url.origin !== currentOrigin)
|
|
46
|
-
return;
|
|
47
|
-
event.preventDefault();
|
|
48
|
-
globalThis.history.pushState({}, '', anchor.href);
|
|
49
|
-
onNavigate();
|
|
50
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/test-types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/types.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Component, Snippet } from 'svelte';
|
|
2
|
-
type BaseProps = Record<string, any>;
|
|
3
|
-
export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
|
|
4
|
-
default: Component<Props>;
|
|
5
|
-
}>;
|
|
6
|
-
export type RouteComponent<Props extends BaseProps = BaseProps> = Component<Props> | LazyRouteComponent<Props>;
|
|
7
|
-
export type LayoutComponent = RouteComponent<{
|
|
8
|
-
children: Snippet;
|
|
9
|
-
}>;
|
|
10
|
-
export type Routes = {
|
|
11
|
-
[_: `/${string}`]: RouteComponent | Routes;
|
|
12
|
-
'*'?: RouteComponent;
|
|
13
|
-
layout?: LayoutComponent;
|
|
14
|
-
};
|
|
15
|
-
export type Path<T extends Routes> = RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>>>;
|
|
16
|
-
export type PathParams<T extends string> = ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
|
|
17
|
-
export type AllParams<T extends Routes> = Partial<Record<ExtractParams<RecursiveKeys<T>>, string>>;
|
|
18
|
-
type StripNonRoutes<T extends Routes> = {
|
|
19
|
-
[K in keyof T as K extends '*' ? never : K extends 'layout' ? never : K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
20
|
-
};
|
|
21
|
-
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|
|
22
|
-
[K in keyof T]: K extends string ? T[K] extends Routes ? RecursiveKeys<T[K], `${Prefix}${K}`> : `${Prefix}${K}` : never;
|
|
23
|
-
}[keyof T];
|
|
24
|
-
type RemoveLastSlash<T extends string> = T extends '/' ? T : T extends `${infer R}/` ? R : T;
|
|
25
|
-
type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? Param : never;
|
|
26
|
-
export {};
|
package/dist/types/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|