sv-router 0.0.1 → 0.0.2

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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Component } from 'svelte';
3
3
  import RecursiveComponentTree from './RecursiveComponentTree.svelte';
4
- import { paramsStore } from './router.svelte.ts';
4
+ import { paramsStore } from './router.svelte.js';
5
5
 
6
6
  let { tree }: { tree: Component[] } = $props();
7
7
 
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { on } from 'svelte/events';
3
3
  import RecursiveComponentTree from './RecursiveComponentTree.svelte';
4
- import { componentTree, onGlobalClick, onNavigate } from './router.svelte.ts';
4
+ import { componentTree, onGlobalClick, onNavigate } from './router.svelte.js';
5
5
 
6
6
  onNavigate();
7
7
 
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { writeRouterCode } from "../gen/write-router-code.js";
3
+ writeRouterCode();
@@ -0,0 +1,5 @@
1
+ export declare const ROUTES_PATH = "src/routes";
2
+ export declare const GEN_CODE_DIR_PATH = ".router";
3
+ export declare const ROUTER_PATH: string;
4
+ export declare const TSCONFIG_PATH: string;
5
+ export declare const GEN_CODE_ALIAS = "sv-router/generated";
package/dist/common.js ADDED
@@ -0,0 +1,6 @@
1
+ import path from 'node:path';
2
+ export const ROUTES_PATH = 'src/routes';
3
+ export const GEN_CODE_DIR_PATH = '.router';
4
+ export const ROUTER_PATH = path.join(GEN_CODE_DIR_PATH, '/router.ts');
5
+ export const TSCONFIG_PATH = path.join(GEN_CODE_DIR_PATH, '/tsconfig.json');
6
+ export const GEN_CODE_ALIAS = 'sv-router/generated';
@@ -0,0 +1,12 @@
1
+ type FileTree = (string | {
2
+ name: string;
3
+ tree: FileTree;
4
+ })[];
5
+ type GeneratedRoutes = {
6
+ [key: string]: string | GeneratedRoutes;
7
+ };
8
+ export declare function generateRouterCode(routesPath: string): string;
9
+ export declare function buildFileTree(routesPath: string): FileTree;
10
+ export declare function createRouteMap(fileTree: FileTree, prefix?: string): GeneratedRoutes;
11
+ export declare function createRouterCode(routes: GeneratedRoutes, routesPath: string): string;
12
+ export {};
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const PARAM_FILENAME_REGEX = /\[(.*)\].svelte/g;
4
+ export function generateRouterCode(routesPath) {
5
+ const fileTree = buildFileTree(path.join(process.cwd(), routesPath));
6
+ const routeMap = createRouteMap(fileTree);
7
+ return createRouterCode(routeMap, path.join('..', routesPath));
8
+ }
9
+ export function buildFileTree(routesPath) {
10
+ const entries = fs.readdirSync(routesPath);
11
+ const result = [];
12
+ for (const entry of entries) {
13
+ const stat = fs.lstatSync(path.join(routesPath, entry));
14
+ if (stat.isDirectory()) {
15
+ result.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
16
+ }
17
+ else if (entry.endsWith('.svelte')) {
18
+ result.push(entry);
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ export function createRouteMap(fileTree, prefix = '') {
24
+ const result = {};
25
+ for (const entry of fileTree) {
26
+ if (typeof entry === 'string') {
27
+ switch (entry) {
28
+ case 'index.svelte': {
29
+ result['/'] = prefix + entry;
30
+ break;
31
+ }
32
+ case '*.svelte': {
33
+ result['*'] = prefix + entry;
34
+ break;
35
+ }
36
+ case '_layout.svelte': {
37
+ result['layout'] = prefix + entry;
38
+ break;
39
+ }
40
+ default: {
41
+ if (PARAM_FILENAME_REGEX.test(entry)) {
42
+ result['/' + entry.replaceAll(PARAM_FILENAME_REGEX, ':$1')] = prefix + entry;
43
+ break;
44
+ }
45
+ result['/' + entry.replace('.svelte', '')] = prefix + entry;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ else {
51
+ result['/' + entry.name] = createRouteMap(entry.tree, prefix + entry.name + '/');
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+ export function createRouterCode(routes, routesPath) {
57
+ if (!routesPath.endsWith('/')) {
58
+ routesPath += '/';
59
+ }
60
+ const jsonRoutes = JSON.stringify(routes, undefined, 2);
61
+ const withImports = jsonRoutes.replaceAll(/"(.*)": "(.*)",?/g, `"$1": () => import("${routesPath}$2"),`);
62
+ return [
63
+ 'import { createRouter } from "sv-router";',
64
+ '\n\n',
65
+ `export const { path, goto, params } = createRouter(${withImports});`,
66
+ ].join('');
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { buildFileTree, createRouteMap, createRouterCode, generateRouterCode, } from "./generate-router-code.js";
2
+ vi.mock('node:fs', () => ({
3
+ default: {
4
+ readdirSync: (dir) => {
5
+ if (dir.toString().endsWith('posts')) {
6
+ return [
7
+ '[id].svelte',
8
+ '_layout.svelte',
9
+ 'index.svelte',
10
+ 'static.svelte',
11
+ 'text.txt',
12
+ 'noextension',
13
+ ];
14
+ }
15
+ return ['*.svelte', 'about.svelte', 'index.svelte', 'posts'];
16
+ },
17
+ lstatSync: (dir) => ({
18
+ isDirectory: () => dir.toString().endsWith('posts'),
19
+ }),
20
+ },
21
+ }));
22
+ describe('generateRouterCode', () => {
23
+ it('should generate the router code', () => {
24
+ const result = generateRouterCode('./a/fake/path');
25
+ expect(result).toBe(`import { createRouter } from "sv-router";
26
+
27
+ export const { path, goto, params } = createRouter({
28
+ "*": () => import("../a/fake/path/*.svelte"),
29
+ "/about": () => import("../a/fake/path/about.svelte"),
30
+ "/": () => import("../a/fake/path/index.svelte"),
31
+ "/posts": {
32
+ "/:id": () => import("../a/fake/path/posts/[id].svelte"),
33
+ "layout": () => import("../a/fake/path/posts/_layout.svelte"),
34
+ "/": () => import("../a/fake/path/posts/index.svelte"),
35
+ "/static": () => import("../a/fake/path/posts/static.svelte"),
36
+ }
37
+ });`);
38
+ });
39
+ });
40
+ describe('buildFileTree', () => {
41
+ it('should get the file tree', () => {
42
+ const result = buildFileTree('a/fake/path');
43
+ expect(result).toEqual([
44
+ '*.svelte',
45
+ 'about.svelte',
46
+ 'index.svelte',
47
+ {
48
+ name: 'posts',
49
+ tree: ['[id].svelte', '_layout.svelte', 'index.svelte', 'static.svelte'],
50
+ },
51
+ ]);
52
+ });
53
+ });
54
+ describe('createRouteMap', () => {
55
+ it('should generate routes', () => {
56
+ const result = createRouteMap([
57
+ 'index.svelte',
58
+ 'about.svelte',
59
+ {
60
+ name: 'posts',
61
+ tree: ['index.svelte', 'static.svelte', '[id].svelte', '_layout.svelte'],
62
+ },
63
+ '*.svelte',
64
+ ]);
65
+ expect(result).toEqual({
66
+ '/': 'index.svelte',
67
+ '/about': 'about.svelte',
68
+ '/posts': {
69
+ '/': 'posts/index.svelte',
70
+ '/static': 'posts/static.svelte',
71
+ '/:id': 'posts/[id].svelte',
72
+ layout: 'posts/_layout.svelte',
73
+ },
74
+ '*': '*.svelte',
75
+ });
76
+ });
77
+ });
78
+ describe('createRouterCode', () => {
79
+ it('should generate the router', () => {
80
+ const result = createRouterCode({
81
+ '/': 'index.svelte',
82
+ '/about': 'about.svelte',
83
+ '/posts': {
84
+ '/': 'posts/index.svelte',
85
+ '/static': 'posts/static.svelte',
86
+ '/:id': 'posts/:id.svelte',
87
+ },
88
+ '*': '*.svelte',
89
+ }, './routes');
90
+ expect(result).toBe(`import { createRouter } from "sv-router";
91
+
92
+ export const { path, goto, params } = createRouter({
93
+ "/": () => import("./routes/index.svelte"),
94
+ "/about": () => import("./routes/about.svelte"),
95
+ "/posts": {
96
+ "/": () => import("./routes/posts/index.svelte"),
97
+ "/static": () => import("./routes/posts/static.svelte"),
98
+ "/:id": () => import("./routes/posts/:id.svelte"),
99
+ },
100
+ "*": () => import("./routes/*.svelte"),
101
+ });`);
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export declare function writeRouterCode(): void;
@@ -0,0 +1,36 @@
1
+ /* eslint-disable no-console */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { GEN_CODE_ALIAS, GEN_CODE_DIR_PATH, ROUTER_PATH, ROUTES_PATH, TSCONFIG_PATH, } from "../common.js";
5
+ import { generateRouterCode } from "./generate-router-code.js";
6
+ export function writeRouterCode() {
7
+ try {
8
+ if (!fs.existsSync(GEN_CODE_DIR_PATH)) {
9
+ fs.mkdirSync(GEN_CODE_DIR_PATH);
10
+ }
11
+ // Write `.router/router.ts` file
12
+ const routerCode = generateRouterCode(ROUTES_PATH);
13
+ writeFileIfDifferent(ROUTER_PATH, routerCode);
14
+ // Write `.router/tsconfig.json` file
15
+ const tsConfig = {
16
+ compilerOptions: {
17
+ module: 'Preserve',
18
+ moduleResolution: 'Bundler',
19
+ paths: {
20
+ [GEN_CODE_ALIAS]: [path.join('..', ROUTER_PATH)],
21
+ },
22
+ },
23
+ include: ['./router.ts'],
24
+ };
25
+ writeFileIfDifferent(TSCONFIG_PATH, JSON.stringify(tsConfig, undefined, 2));
26
+ console.log('✅️ Routes generated');
27
+ }
28
+ catch (error) {
29
+ console.error('Error during routes generation:', error);
30
+ }
31
+ }
32
+ function writeFileIfDifferent(filePath, content) {
33
+ if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
34
+ fs.writeFileSync(filePath, content);
35
+ }
36
+ }
@@ -21,32 +21,33 @@ export function matchRoute(pathname, routes) {
21
21
  else if (routePart !== pathPart) {
22
22
  break;
23
23
  }
24
+ if (index !== routeParts.length - 1) {
25
+ continue;
26
+ }
24
27
  const routeMatch = routes[routeParts.join('/')];
25
28
  if (typeof routeMatch !== 'function' &&
26
29
  routeMatch?.layout &&
27
30
  !layouts.includes(routeMatch.layout)) {
28
31
  layouts.push(routeMatch.layout);
29
32
  }
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
- }
33
+ if (typeof routeMatch === 'function') {
34
+ if (routeParts.length === pathParts.length) {
35
+ match = routeMatch;
38
36
  }
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
- }
37
+ else {
38
+ continue;
39
+ }
40
+ }
41
+ else if (routeMatch) {
42
+ const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
43
+ const result = matchRoute(nestedPathname, routeMatch);
44
+ if (result) {
45
+ match = result.match;
46
+ params = { ...params, ...result.params };
47
+ layouts.push(...result.layouts);
47
48
  }
48
- break outer;
49
49
  }
50
+ break outer;
50
51
  }
51
52
  }
52
53
  return { match, layouts, params };
@@ -48,7 +48,10 @@ describe('matchRoute', () => {
48
48
  },
49
49
  layout: Layout1,
50
50
  },
51
- '/users/*': UserNotFound,
51
+ '/users': {
52
+ '*': UserNotFound,
53
+ layout: Layout1,
54
+ },
52
55
  '*': PageNotFound,
53
56
  },
54
57
  },
@@ -66,12 +69,16 @@ describe('matchRoute', () => {
66
69
  '/static': StaticPost,
67
70
  layout: Layout1,
68
71
  },
69
- '/users/*': UserNotFound,
72
+ '/users': {
73
+ '*': UserNotFound,
74
+ layout: Layout1,
75
+ },
70
76
  '/': Home,
71
77
  },
72
78
  },
73
79
  ])('$mode paths', ({ mode, routes: r }) => {
74
80
  const routes = r;
81
+ const treeMode = mode.startsWith('tree');
75
82
  it('should match the root route', () => {
76
83
  const { match } = matchRoute('/', routes);
77
84
  expect(match).toEqual(Home);
@@ -98,7 +105,7 @@ describe('matchRoute', () => {
98
105
  expect(match).toEqual(DynamicPostComment);
99
106
  expect(params).toEqual({ id: 'bar', commentId: 'baz' });
100
107
  });
101
- if (mode === 'nested') {
108
+ if (treeMode) {
102
109
  it('should match routes with layout', () => {
103
110
  const { layouts: layouts1 } = matchRoute('/', routes);
104
111
  const { layouts: layouts2 } = matchRoute('/posts', routes);
@@ -110,13 +117,19 @@ describe('matchRoute', () => {
110
117
  expect(layouts4).toEqual([Layout1, Layout2]);
111
118
  });
112
119
  }
113
- it('should match wildcard nested route', () => {
114
- const { match } = matchRoute('/users/notfound', routes);
115
- expect(match).toEqual(UserNotFound);
116
- });
117
120
  it('should match wildcard route', () => {
118
- const { match } = matchRoute('/notfound', routes);
121
+ const { match, layouts } = matchRoute('/notfound', routes);
119
122
  expect(match).toEqual(PageNotFound);
123
+ if (treeMode) {
124
+ expect(layouts).toEqual([]);
125
+ }
126
+ });
127
+ it('should match wildcard nested route', () => {
128
+ const { match, layouts } = matchRoute('/users/notfound', routes);
129
+ expect(match).toEqual(UserNotFound);
130
+ if (treeMode) {
131
+ expect(layouts).toEqual([Layout1]);
132
+ }
120
133
  });
121
134
  it('should not match any route', () => {
122
135
  delete routes['*'];
@@ -1,5 +1,8 @@
1
1
  import type { Component } from 'svelte';
2
+ import type { PathParams } from '../types/types.ts';
2
3
  import type { LazyRouteComponent, RouteComponent } from '../types/types.ts';
4
+ export type ConstructPathArgs<T extends string> = PathParams<T> extends never ? [T] : [T, PathParams<T>];
5
+ export declare function constructPath<T extends string>(...args: ConstructPathArgs<T>): string;
3
6
  export declare function resolveRouteComponents(input: RouteComponent<any>[]): Promise<Component[]>;
4
7
  export declare function resolveRouteComponent(input: RouteComponent): Promise<Component>;
5
8
  export declare function isLazyImport(input: unknown): input is LazyRouteComponent;
@@ -1,3 +1,13 @@
1
+ export function constructPath(...args) {
2
+ const [path, params] = args;
3
+ if (!params)
4
+ return path;
5
+ let result = path;
6
+ for (const key in params) {
7
+ result = result.replace(`:${key}`, params[key]);
8
+ }
9
+ return result;
10
+ }
1
11
  export function resolveRouteComponents(input) {
2
12
  return Promise.all(input.map((c) => resolveRouteComponent(c)));
3
13
  }
@@ -1,5 +1,5 @@
1
1
  import type { Component } from 'svelte';
2
- import { type ConstructPathArgs } from './helpers/construct-path.ts';
2
+ import { type ConstructPathArgs } from './helpers/utils.ts';
3
3
  import type { AllParams, Path, Routes } from './types/types.ts';
4
4
  export declare let routes: Routes;
5
5
  export declare const componentTree: Component<{}, {}, string>[];
@@ -1,14 +1,13 @@
1
1
  import { BROWSER, DEV } from 'esm-env';
2
- import { constructPath } from "./helpers/construct-path.js";
3
2
  import { matchRoute } from "./helpers/match-route.js";
4
- import { resolveRouteComponents } from "./helpers/utils.js";
3
+ import { constructPath, resolveRouteComponents } from "./helpers/utils.js";
5
4
  export let routes;
6
5
  export const componentTree = $state([]);
7
6
  export const paramsStore = $state({});
8
7
  export function createRouter(r) {
9
8
  routes = r;
10
9
  if (DEV && BROWSER) {
11
- import('./helpers/validate-routes.ts').then(({ validateRoutes }) => {
10
+ import('./helpers/validate-routes.js').then(({ validateRoutes }) => {
12
11
  validateRoutes(routes);
13
12
  });
14
13
  }
@@ -0,0 +1 @@
1
+ export { router } from './plugin.ts';
@@ -0,0 +1 @@
1
+ export { router } from "./plugin.js";
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function router(): Plugin;
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import { GEN_CODE_ALIAS, ROUTER_PATH, ROUTES_PATH } from "../common.js";
3
+ import { writeRouterCode } from "../gen/write-router-code.js";
4
+ export function router() {
5
+ return {
6
+ name: 'sv-router',
7
+ config(config) {
8
+ if (!config.resolve) {
9
+ config.resolve = {};
10
+ }
11
+ if (!config.resolve.alias) {
12
+ config.resolve.alias = {};
13
+ }
14
+ const replacement = path.resolve(process.cwd(), ROUTER_PATH);
15
+ if (Array.isArray(config.resolve.alias)) {
16
+ config.resolve.alias.push({ find: GEN_CODE_ALIAS, replacement });
17
+ }
18
+ else {
19
+ config.resolve.alias[GEN_CODE_ALIAS] = replacement;
20
+ }
21
+ },
22
+ buildStart() {
23
+ writeRouterCode();
24
+ },
25
+ watchChange(file) {
26
+ if (file.includes(ROUTES_PATH)) {
27
+ writeRouterCode();
28
+ }
29
+ },
30
+ };
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Modern Svelte routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -23,14 +23,21 @@
23
23
  "import": "./dist/index.js",
24
24
  "svelte": "./dist/index.js",
25
25
  "types": "./dist/index.d.ts"
26
+ },
27
+ "./vite-plugin": {
28
+ "import": "./dist/vite-plugin/index.js",
29
+ "types": "./dist/vite-plugin/index.d.ts"
26
30
  }
27
31
  },
32
+ "bin": "./dist/cli/index.js",
28
33
  "files": [
29
34
  "dist"
30
35
  ],
31
36
  "scripts": {
32
37
  "ex:basic": "pnpm --filter basic-example",
38
+ "ex:file-based": "pnpm --filter file-based-example",
33
39
  "build": "svelte-package -i src && rm -rf .svelte-kit",
40
+ "watch": "svelte-package -i src --watch",
34
41
  "test": "vitest",
35
42
  "check": "tsc --noEmit && pnpm -r check",
36
43
  "lint": "eslint .",
@@ -46,6 +53,7 @@
46
53
  "devDependencies": {
47
54
  "@eslint/js": "^9.15.0",
48
55
  "@sveltejs/package": "^2.3.7",
56
+ "@types/node": "^22.10.1",
49
57
  "eslint-config-prettier": "^9.1.0",
50
58
  "eslint-plugin-simple-import-sort": "^12.1.1",
51
59
  "eslint-plugin-svelte": "^2.46.0",
@@ -63,5 +71,5 @@
63
71
  "peerDependencies": {
64
72
  "svelte": "^5"
65
73
  },
66
- "packageManager": "pnpm@9.14.2"
74
+ "packageManager": "pnpm@9.15.0"
67
75
  }
@@ -1,3 +0,0 @@
1
- import type { PathParams } from '../types/types.ts';
2
- export type ConstructPathArgs<T extends string> = PathParams<T> extends never ? [T] : [T, PathParams<T>];
3
- export declare function constructPath<T extends string>(...args: ConstructPathArgs<T>): string;
@@ -1,10 +0,0 @@
1
- export function constructPath(...args) {
2
- const [path, params] = args;
3
- if (!params)
4
- return path;
5
- let result = path;
6
- for (const key in params) {
7
- result = result.replace(`:${key}`, params[key]);
8
- }
9
- return result;
10
- }