Skip to content

Commit

Permalink
Merge pull request #9 from mateothegreat/feat/base-path
Browse files Browse the repository at this point in the history
added support for basePath
  • Loading branch information
mateothegreat authored Nov 17, 2024
2 parents ffd2e33 + 6955a9b commit 5d4af32
Show file tree
Hide file tree
Showing 8 changed files with 642 additions and 886 deletions.
1,371 changes: 539 additions & 832 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
},
"devDependencies": {
"@sveltejs/package": "^2.3.7",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@tsconfig/svelte": "^5.0.4",
"@types/node": "^22.8.1",
"@types/node": "^22.9.0",
"@vitest/ui": "^2.1.5",
"cpx": "^1.5.0",
"jsdom": "^25.0.1",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8",
Expand All @@ -32,8 +34,7 @@
"tailwind-merge": "^2.5.4",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.4"
"vitest": "^2.1.5"
},
"peerDependencies": {
"svelte": "^5.0.0"
Expand Down
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ An SPA router for Svelte that allows you to divide & conquer your app with neste
- Use components, snippets, or both!
- Use regex paths (e.g. `/foo/(.*?)/bar`) and/or named parameters together 🔥.
- Use async routes simply with `component: async () => import("./my-component.svelte")`.
- Base path support.

## Installation

Expand All @@ -26,6 +27,18 @@ All you need to do is define your routes and then use the `Router` component wit

To make a link, use the `route` directive with the `href` attribute such as `<a use:route href="/foo">foo</a>`.

## Base Paths

In some cases you may not be able to use the base path of your app as the root path. For example, if you are using a nested router, you may want to use a base path of `/foo` instead of `/`.

Simply pass the `basePath` prop to the `Router` component and it will handle the rest:

> No need to update your routes either, it will support both `/mybasepath/foo` and `/foo` just fine.
```svelte
<Router basePath="/mybasepath" {routes} />
```

### Methods

#### `goto(path: string)`
Expand Down
33 changes: 33 additions & 0 deletions src/lib/instance.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { Instance } from './instance.svelte';

const instance1 = new Instance("", [
{
path: "/1a",
component: "1a",
},
{
path: "/1b",
component: "1b",
},
]);

const instance2 = new Instance("/foo", [
{
path: "/2a",
component: "2a",
},
{
path: "/2b",
component: "2b",
},
]);

describe("Instance.routes", () => {
it("should navigate", async () => {
expect(await instance1.get("/1a").path).toEqual(instance1.routes[0].path);
expect(await instance2.get("/2a").path).toEqual(instance2.routes[0].path);
expect(await instance1.get("/1b").path).toEqual(instance1.routes[1].path);
expect(await instance2.get("/foo/2b").path).toEqual(instance2.routes[1].path);
});
});
88 changes: 43 additions & 45 deletions src/lib/instance.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface Route {
* A router instance that each <Router/> component creates.
*/
export class Instance {
id = crypto.randomUUID();
basePath?: string;
routes: Route[] = [];
#pre?: PreHooks;
#post?: PostHooks;
Expand All @@ -25,20 +27,55 @@ export class Instance {

/**
* Creates a new router instance.
* @param {string} basePath (optional) The base path to navigate to.
* @param {Route[]} routes The routes to navigate to.
* @param {PreHooks} pre (optional) The pre hooks to run before navigating to a route.
* @param {PostHooks} post (optional) The post hooks to run after navigating to a route.
* @param {string} currentPath (optional) The current path to automaticallynavigate to.
*/
constructor(routes: Route[], pre?: PreHooks, post?: PostHooks) {
constructor(basePath: string, routes: Route[], pre?: PreHooks, post?: PostHooks, currentPath?: string) {
this.basePath = basePath;
this.routes = routes;
this.current = get(this, this.routes, location.pathname);
if (currentPath) {
this.current = this.get(currentPath);
}
this.#pre = pre;
this.#post = post;
}

/**
* Get the route for a given path.
* @returns { Route } The route for the given path.
*/
get(path: string): Route | undefined {
let route: Route | undefined;

let pathToMatch = path;
if (this.basePath && this.basePath !== "/") {
pathToMatch = path.replace(this.basePath, "");
}
// If the path is the root path, return the root route:
if (pathToMatch === "/") {
route = this.routes.find((route) => route.path === "/");
}
console.log(pathToMatch);
// Split the path into the first segment and the rest:
const [first, ...rest] = pathToMatch.replace(/^\//, "").split("/");
route = this.routes.find((route) => route.path === first);

// If the route is not found, try to find a route that matches at least part of the path:
if (!route) {
for (const r of this.routes) {
const regexp = new RegExp(r.path);
const match = regexp.exec(path);
if (match) {
route = { ...r, params: match.slice(1) };
break;
}
}
}

// Setup a history watcher to navigate to the current route:
// window.addEventListener("pushState", (event: Event) => {
// this.run(get(this, this.routes, location.pathname));
// });
return route;
}

/**
Expand Down Expand Up @@ -107,45 +144,6 @@ export class Instance {
}
}

/**
* Get the route for a given path.
* @param {Instance} routerInstance The router instance to get the route for.
* @param {Route[]} routes The routes to get the route for.
* @param {string} path The path to get the route for.
* @param {ParentRoute} parent The parent route to get the route for.
* @returns {Route} The route for the given path.
*/
export const get = (
routerInstance: Instance,
routes: Route[],
path: string,
): Route => {
let route: Route;

// If the path is the root path, return the root route:
if (path === "/") {
route = routes.find((route) => route.path === "/");
}

// Split the path into the first segment and the rest:
const [first, ...rest] = path.replace(/^\//, "").split("/");
route = routes.find((route) => route.path === first);

// If the route is not found, try to find a route that matches the path:
if (!route) {
for (const r of routes) {
const regexp = new RegExp(r.path);
const match = regexp.exec(path);
if (match) {
route = { ...r, params: match.slice(1) };
break;
}
}
}

return route;
};

/**
* Sets up a new history watcher for a router instance.
* @param {Instance} instance The router instance to setup the history watcher for.
Expand Down
9 changes: 5 additions & 4 deletions src/lib/router.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<script lang="ts">
import { mount, onDestroy, unmount, type Component } from "svelte";
import { get, Instance, setupHistoryWatcher, type PostHooks, type PreHooks, type Route } from "./instance.svelte";
import { Instance, setupHistoryWatcher, type PostHooks, type PreHooks, type Route } from "./instance.svelte";
type Props = {
basePath?: string;
pre?: PreHooks;
post?: PostHooks;
routes: Route[];
navigating?: boolean;
instance?: Instance;
};
let { routes, pre, post, navigating = $bindable(), instance = $bindable() }: Props = $props();
let { basePath, routes, pre, post, navigating = $bindable(), instance = $bindable() }: Props = $props();
// Initialize the instance
instance = new Instance(routes, pre, post);
instance = new Instance(basePath, routes, pre, post);
// Setup history watcher which updates the instance's current
// route based on `pushState` and `popState` events.
Expand All @@ -26,7 +27,7 @@
});
// Set up the initial route so that the component is rendered.
const route = get(instance, routes, location.pathname);
const route = instance.get(location.pathname);
if (route) {
instance.run(route);
}
Expand Down
2 changes: 1 addition & 1 deletion test/app/src/app.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
<div class=" w-full flex-1 bg-zinc-900 p-6">
<div class="flex flex-col gap-4 rounded-lg bg-zinc-950 p-4 shadow-xl">
<p class="text-center text-xs text-zinc-500">app.svelte</p>
<Router bind:navigating {routes} pre={globalAuthGuardHook} post={globalLoggerPostHook} />
<Router basePath="/mybasepath" bind:navigating {routes} pre={globalAuthGuardHook} post={globalLoggerPostHook} />
</div>
</div>
</div>
3 changes: 3 additions & 0 deletions test/app/src/lib/foo/bar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="bg-indigo-400 p-10">
<h1>/bar path off of basePath: /foo</h1>
</div>

0 comments on commit 5d4af32

Please sign in to comment.