├── .gitignore ├── .npmignore ├── tsconfig.json ├── src ├── lib │ ├── utils │ │ ├── createIdIssuer.ts │ │ ├── getPathSegments.ts │ │ ├── getPathWithoutBase.ts │ │ └── linkHandle.ts │ ├── options.ts │ ├── components │ │ ├── Link.svelte │ │ └── Route.svelte │ ├── router.ts │ └── location.ts └── index.ts ├── .prettierrc ├── svelte.config.js ├── .github └── workflows │ └── npm-publish.yml ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .svelte-kit 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .svelte-kit 3 | node_modules 4 | src 5 | .prettierrc 6 | package-lock.json 7 | svelte.config.js 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "strict": true, 6 | "forceConsistentCasingInFileNames": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils/createIdIssuer.ts: -------------------------------------------------------------------------------- 1 | export type CreateIdIssuer = () => () => number 2 | 3 | export const createIdIssuer: CreateIdIssuer = () => { 4 | let id = 0 5 | return () => id++ 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "printWidth": 128, 7 | "svelteSortOrder": "scripts-markup-styles" 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils/getPathSegments.ts: -------------------------------------------------------------------------------- 1 | export type GetPathSegments = (path: string) => string[] 2 | 3 | export const getPathSegments: GetPathSegments = (path) => { 4 | return path.split(/(?=\/)/) 5 | } 6 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | preprocess: vitePreprocess(), 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /src/lib/utils/getPathWithoutBase.ts: -------------------------------------------------------------------------------- 1 | export type GetPathWithoutBase = (path: string, basePath: string | null) => string 2 | 3 | export const getPathWithoutBase: GetPathWithoutBase = (path, basePath) => { 4 | if (basePath === null) return path 5 | return path.startsWith(basePath) ? path.slice(basePath.length) : path 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { router, type Router } from './lib/router' 2 | export { options, type OptionsStore, type Options } from './lib/options' 3 | export { path, type PathStore, type Path } from './lib/location' 4 | export { query, type QueryStore, type Query } from './lib/location' 5 | export { hash, type HashStore, type Hash } from './lib/location' 6 | export { default as Route } from './lib/components/Route.svelte' 7 | export { default as Link } from './lib/components/Link.svelte' 8 | export { linkHandle, type LinkHandle } from './lib/utils/linkHandle' 9 | export { getPathSegments, type GetPathSegments } from './lib/utils/getPathSegments' 10 | -------------------------------------------------------------------------------- /src/lib/options.ts: -------------------------------------------------------------------------------- 1 | import { writable, type Readable } from 'svelte/store' 2 | 3 | export type Options = { 4 | mode: 'window' | 'hash' 5 | basePath: null | string 6 | } 7 | 8 | export type OptionsStore = { 9 | subscribe: Readable['subscribe'] 10 | set: (changedOptions: Partial) => void 11 | } 12 | 13 | const createOptions = (initialValues: Options): OptionsStore => { 14 | const { subscribe, update } = writable(initialValues) 15 | 16 | return { 17 | subscribe, 18 | 19 | set: (changedOptions = {}) => { 20 | update((options) => Object.assign(options, changedOptions)) 21 | }, 22 | } 23 | } 24 | 25 | export const options = createOptions({ 26 | mode: 'window', 27 | basePath: null, 28 | }) 29 | -------------------------------------------------------------------------------- /src/lib/components/Link.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Publish to NPM 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 19 18 | - run: npm ci 19 | - run: npm run build 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 19 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /src/lib/router.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'svelte/store' 2 | import { options, type Options } from './options' 3 | 4 | const dispatchLocationChange = (mode: Options['mode'] = get(options).mode): void => { 5 | let type: string = 'popstate' 6 | 7 | if (mode === 'window') type = 'popstate' 8 | if (mode === 'hash') type = 'hashchange' 9 | 10 | window.dispatchEvent(new Event(type)) 11 | } 12 | 13 | export type Router = { 14 | go: (delta?: number) => void 15 | push: (url?: string | URL | null, state?: any) => void 16 | replace: (url?: string | URL | null, state?: any) => void 17 | } 18 | 19 | export const router: Router = { 20 | go: (delta: number = 0) => { 21 | history.go(delta) 22 | dispatchLocationChange() 23 | }, 24 | 25 | push: (url, state = null) => { 26 | history.pushState(state, '', url) 27 | dispatchLocationChange() 28 | }, 29 | 30 | replace: (url, state = null) => { 31 | history.replaceState(state, '', url) 32 | dispatchLocationChange() 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ayndqy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/utils/linkHandle.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from 'svelte/action' 2 | import { router } from '../router' 3 | 4 | export const linkClickHandler = (event: MouseEvent): boolean | void => { 5 | const target = (event.target as HTMLElement)?.closest('a[href]') as HTMLAnchorElement 6 | const href = target?.href 7 | 8 | if (target === null || href === null) return true 9 | 10 | const isIgnored = ['', 'true'].includes(target.getAttribute('data-handle-ignore') ?? 'false') 11 | const isTargetNonSelf = (target.getAttribute('target') ?? '_self') !== '_self' 12 | const isKeyPressed = event.metaKey || event.ctrlKey || event.altKey || event.shiftKey 13 | const isExternalOrigin = new URL(href).origin !== document.location.origin 14 | 15 | if (isIgnored || isTargetNonSelf || isKeyPressed || isExternalOrigin) return true 16 | 17 | href === document.location.href ? router.replace(href) : router.push(href) 18 | event.preventDefault() 19 | } 20 | 21 | export type LinkHandle = Action 22 | 23 | export const linkHandle: LinkHandle = (node) => { 24 | node.addEventListener('click', linkClickHandler) 25 | 26 | return { 27 | destroy: () => { 28 | node.removeEventListener('click', linkClickHandler) 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-micro", 3 | "version": "2.5.7", 4 | "description": "Light & reactive router for Svelte", 5 | "author": "ayndqy", 6 | "license": "MIT", 7 | "homepage": "https://github.com/ayndqy/svelte-micro#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ayndqy/svelte-micro.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/ayndqy/svelte-micro/issues" 14 | }, 15 | "keywords": [ 16 | "svelte", 17 | "router" 18 | ], 19 | "type": "module", 20 | "main": "./dist/index.js", 21 | "svelte": "./dist/index.js", 22 | "types": "./dist/index.d.ts", 23 | "exports": { 24 | "./package.json": "./package.json", 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "default": "./dist/index.js", 28 | "svelte": "./dist/index.js" 29 | } 30 | }, 31 | "scripts": { 32 | "build": "npx svelte-kit sync && npx svelte-package --input ./src --output ./dist --types", 33 | "prepublishOnly": "npm run build" 34 | }, 35 | "peerDependencies": { 36 | "svelte": "^3.54.0 || ^4.0.0" 37 | }, 38 | "devDependencies": { 39 | "@sveltejs/kit": "^2.5.0", 40 | "@sveltejs/package": "^2.3.0", 41 | "tslib": "^2.6.0", 42 | "typescript": "^5.5.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/location.ts: -------------------------------------------------------------------------------- 1 | import { readable, derived, type Readable } from 'svelte/store' 2 | import { options } from './options' 3 | 4 | export type Path = string 5 | export type Query = string 6 | export type Hash = string 7 | 8 | type Location = { 9 | path: Path 10 | query: Query 11 | hash: Hash 12 | } 13 | 14 | const parseLocation = (fragment: string): Location => { 15 | const pathMatch = fragment.match(/^(\/[^?#]*)?/) 16 | const queryMatch = fragment.match(/\?([^#]*)?/) 17 | const hashMatch = fragment.match(/#(.*)?/) 18 | 19 | return { 20 | path: pathMatch?.[1] || '/', 21 | query: queryMatch?.[1] ? `?${queryMatch?.[1]}` : '', 22 | hash: hashMatch?.[1] ? `#${hashMatch?.[1]}` : '', 23 | } 24 | } 25 | 26 | const getWindowLocation = (): Location => { 27 | const { pathname, search, hash } = document.location 28 | return { path: pathname, query: search, hash: hash } 29 | } 30 | 31 | const getHashLocation = (): Location => { 32 | let hashFragment = document.location.hash.substring(1) 33 | if (hashFragment[0] !== '/') hashFragment = '/' + hashFragment 34 | return parseLocation(hashFragment) 35 | } 36 | 37 | const windowLocation = readable(getWindowLocation(), (set) => { 38 | const handler = () => set(getWindowLocation()) 39 | window.addEventListener('popstate', handler) 40 | return () => window.removeEventListener('popstate', handler) 41 | }) 42 | 43 | const hashLocation = readable(getHashLocation(), (set) => { 44 | const handler = () => set(getHashLocation()) 45 | window.addEventListener('hashchange', handler) 46 | return () => window.removeEventListener('hashchange', handler) 47 | }) 48 | 49 | const selectedLocation: Readable = derived( 50 | [options, windowLocation, hashLocation], 51 | ([$options, $windowLocation, $hashLocation], set) => { 52 | if ($options.mode === 'window') set($windowLocation) 53 | if ($options.mode === 'hash') set($hashLocation) 54 | } 55 | ) 56 | 57 | export type PathStore = Readable 58 | export type QueryStore = Readable 59 | export type HashStore = Readable 60 | 61 | export const path: PathStore = derived(selectedLocation, ($location) => $location.path) 62 | export const query: QueryStore = derived(selectedLocation, ($location) => $location.query) 63 | export const hash: HashStore = derived(selectedLocation, ($location) => $location.hash) 64 | -------------------------------------------------------------------------------- /src/lib/components/Route.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 98 | 99 | 122 | 123 | {#if isRouteActive(getPathWithoutBase($globalPath, $options.basePath), $route, $contextNestedRoutes)} 124 | 125 | {/if} 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Micro 2 | 3 | Light & reactive client-side router for Svelte 4 4 | 5 | ## Table of content 6 | 7 | - [Installation](#installation) 8 | - [Example](#example) 9 | - [API](#api) 10 | - [Imports reference](#imports-reference) 11 | - [`router` object](#router-object) 12 | - [`options` store](#options-store) 13 | - [`path` store](#path-store) 14 | - [`query` store](#query-store) 15 | - [`hash` store](#hash-store) 16 | - [`Route` component](#route-component) 17 | - [`Link` component](#link-component) 18 | - [`linkHandle` action](#linkhandle-action) 19 | - [`getPathSegments` function](#getpathsegments-function) 20 | - [Tips](#tips) 21 | - [`path`, `query`, `hash` usage](#path-query-hash-usage) 22 | - [Scroll behavior control](#scroll-behavior-control) 23 | - [Redirect](#redirect) 24 | - [Guarded route](#guarded-route) 25 | 26 | ## Installation 27 | 28 | ``` 29 | npm i svelte-micro 30 | ``` 31 | 32 | ## Example 33 | 34 | ```svelte 35 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 |

Home page

52 |

Make yourself at home.

53 |
54 | 55 | 56 |

Portfolio

57 | 58 | 59 | 60 |

Portfolio main page

61 | Sites 62 | Apps 63 |
64 | 65 | 66 |

Sites

67 | Back to portfolio main page 68 |
69 | 70 | 71 |

Apps

72 | Back to portfolio main page 73 |
74 | 75 | 76 |

The route is not found in /portfolio

77 | Back to portfolio main page 78 |
79 |
80 | 81 | 82 |

Our story

83 |
84 | 85 | 86 |

The route is not found

87 | Back to home 88 |
89 |
90 | ``` 91 | 92 | This code shows the capabilities of the `svelte-micro` routing system.\ 93 | Spend a minute analyzing this example to understand the approach. 94 | 95 | For advanced examples see the [Tips](#tips) section. 96 | 97 | ## API 98 | 99 | ### Imports reference 100 | 101 | | Entity | Related imports | 102 | | ------------------------------------------------------- | ------------------------------------------------------------------------- | 103 | | [`router` object](#router-object) | `import { router, type Router } from 'svelte-micro'` | 104 | | [`options` store](#options-store) | `import { options, type OptionsStore, type Options } from 'svelte-micro'` | 105 | | [`path` store](#path-store) | `import { path, type PathStore, type Path } from 'svelte-micro'` | 106 | | [`query` store](#query-store) | `import { query, type QueryStore, type Query } from 'svelte-micro'` | 107 | | [`hash` store](#hash-store) | `import { hash, type HashStore, type Hash } from 'svelte-micro'` | 108 | | [`Route` component](#route-component) | `import { Route } from 'svelte-micro'` | 109 | | [`Link` component](#link-component) | `import { Link } from 'svelte-micro'` | 110 | | [`linkHandle` action](#linkhandle-action) | `import { linkHandle, type LinkHandle } from 'svelte-micro'` | 111 | | [`getPathSegments` function](#getpathsegments-function) | `import { getPathSegments, type GetPathSegments } from 'svelte-micro'` | 112 | 113 | ### `router` object 114 | 115 | #### Type definition 116 | 117 | ```typescript 118 | type Router = { 119 | go: (delta?: number) => void 120 | push: (url?: string | URL | null, state?: any) => void 121 | replace: (url?: string | URL | null, state?: any) => void 122 | } 123 | ``` 124 | 125 | #### Description 126 | 127 | The `router` object is an object whose methods allow to manipulate history. 128 | 129 | - `router.go`\ 130 | Move on `delta` steps through the history. 131 | 132 | - `router.push`\ 133 | Push new `url` and [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to the history. 134 | 135 | - `router.replace`\ 136 | Replace current `url` and [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) in the history. 137 | 138 | ### `options` store 139 | 140 | #### Type definition 141 | 142 | ```typescript 143 | type OptionsStore = { 144 | subscribe: import('svelte/store').Readable['subscribe'] 145 | set: (changedOptions: Partial) => void 146 | } 147 | ``` 148 | 149 | ```typescript 150 | type Options = { 151 | mode: 'window' | 'hash' 152 | basePath: null | string 153 | } 154 | ``` 155 | 156 | #### Description 157 | 158 | The `options` store provides `subscribe` and `set` methods to access and modify router options. 159 | 160 | - `$options.mode`\ 161 | Default: `'window'`\ 162 | Set the `mode` for the router. 163 | 164 | - `$options.basePath`\ 165 | Default: `null`\ 166 | Set the `basePath` for the router.\ 167 | If a `basePath` value is not found at the beginning of `$path`, the router will continue to operate properly, ignoring the `basePath` option for this state of `$path`. Be aware that if `mode` is set to `'hash'`, the router will try to find the `basePath` value in the hash location fragment, since the hash location fragment is already separated from the path location fragment. 168 | 169 | ### `path` store 170 | 171 | #### Type definition 172 | 173 | ```typescript 174 | type Path = string 175 | ``` 176 | 177 | ```typescript 178 | type PathStore = import('svelte/store').Readable 179 | ``` 180 | 181 | #### Description 182 | 183 | The store which contains current path. 184 | 185 | ### `query` store 186 | 187 | #### Type definition 188 | 189 | ```typescript 190 | type Query = string 191 | ``` 192 | 193 | ```typescript 194 | type QueryStore = import('svelte/store').Readable 195 | ``` 196 | 197 | #### Description 198 | 199 | The store which contains current query. 200 | 201 | ### `hash` store 202 | 203 | #### Type definition 204 | 205 | ```typescript 206 | type Hash = string 207 | ``` 208 | 209 | ```typescript 210 | type HashStore = import('svelte/store').Readable 211 | ``` 212 | 213 | #### Description 214 | 215 | The store which contains current hash. 216 | 217 | ### `Route` component 218 | 219 | #### Type definition 220 | 221 | ```svelte 222 | 226 | 227 | ``` 228 | 229 | #### Description 230 | 231 | The `Route` component defines a route. The props of `Route` are reactive. A nested `Route` component works in context of its parental `Route` component, so you don't need to define its full `path`. 232 | 233 | - `fallback`\ 234 | Default: `{false}` 235 | The property which defines if the route is fallback. A fallback route is active when there is no active routes on its depth. 236 | 237 | - `path`\ 238 | Default: `'/'` 239 | The property which defines route path. `path` must start from `'/'`. 240 | 241 | The top-level (root) `Route` must have `path` equal to `'/'` and `fallback` equal to `false`.\ 242 | These values are set by default, so you can leave them unchanged (see [Example](#example) section). 243 | 244 | ### `Link` component 245 | 246 | #### Type definition 247 | 248 | ```svelte 249 | 253 | 254 | ``` 255 | 256 | #### Description 257 | 258 | The `` component is built on top of [`linkHandle`](#linkhandle-action) and should be used for the internal application navigation.\ 259 | It automatically prevents the window from refreshing. 260 | 261 | - `href`\ 262 | Default: `'/'` 263 | The property which defines link href. 264 | 265 | - `{...restProps}`\ 266 | Any other property is attached on the inner `a` element. 267 | 268 | If the [`basePath` option](#options-store) isn't set to `null`, the `` component will append the `basePath` value to the `href` attribute.\ 269 | If the [`mode` option](#options-store) is set to `"hash"`, the `` component will append a `#` to the beginning of the `href` attribute. 270 | 271 | ### `linkHandle` action 272 | 273 | #### Type definition 274 | 275 | ```typescript 276 | type LinkHandle = import('svelte/action').Action 277 | ``` 278 | 279 | #### Description 280 | 281 | The `linkHandle` action prevents window from refreshing when the click event occurs on a handled `a[href]` element.\ 282 | `linkHandle` can be applied on a parental element to handle nested `a[href]` elements. 283 | 284 | `linkHandle` ignores an `a[href]` element if: 285 | 286 | - `a[href]` has `data-handle-ignore` attribute 287 | - `a[href]` has `target` attribute which isn't equal to `'_self'` 288 | - `a[href]` has external href (`new URL(href).origin !== document.location.origin`) 289 | - `(event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) === true` during the click event 290 | 291 | ### `getPathSegments` function 292 | 293 | #### Type definition 294 | 295 | ```typescript 296 | export type GetPathSegments = (path: string) => string[] 297 | ``` 298 | 299 | #### Description 300 | 301 | The `getPathSegments` function divides `path` into segments. 302 | 303 | For example: `getPathSegments('/about-us/story') => ['/about-us', '/story']`. 304 | 305 | ## Tips 306 | 307 | ### `path`, `query`, `hash` usage 308 | 309 | ```svelte 310 | 320 | 321 | 322 | {text} 323 | 324 | 325 | {#if $hash === '#modal'} 326 | 327 | {/if} 328 | ``` 329 | 330 | ### Scroll behavior control 331 | 332 | ```javascript 333 | import { path } from 'svelte-micro' 334 | 335 | // Disable browser scroll behavior control 336 | if ('scrollRestoration' in history) { 337 | history.scrollRestoration = 'manual' 338 | } 339 | 340 | // On path change reset scroll position 341 | path.subscribe(() => window.scrollTo(0, 0)) 342 | ``` 343 | 344 | By default `svelte-micro` doesn't control scroll behavior, but it's easy to do on your own. 345 | 346 | ### Redirect 347 | 348 | ```svelte 349 | 352 | 353 | 354 | 355 | {router.replace('/redirect-target')} 356 | 357 | 358 | 359 |

You have been redirected

360 |
361 |
362 | ``` 363 | 364 | ### Guarded route 365 | 366 | ```svelte 367 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | {#if isUserAuthenticated} 380 | 381 |

Welcome!

382 | 383 |
384 | {/if} 385 |
386 | ``` 387 | --------------------------------------------------------------------------------