├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __tests__
├── init-usage.spec.ts
├── matcher.spec.ts
├── pre-init-usage.spec.ts
└── tsconfig.json
├── cypress.json
├── cypress
├── fixtures
│ └── example.json
├── integration
│ ├── hash_router.spec.ts
│ ├── history_base_router.spec.ts
│ └── history_router.spec.ts
├── plugins
│ └── index.js
├── support
│ ├── commands.js
│ └── index.js
├── test-app
│ ├── public-hash
│ │ └── index.html
│ ├── public-path-base
│ │ └── index.html
│ ├── public-path
│ │ └── index.html
│ ├── rollup.config.js
│ ├── src
│ │ ├── App.svelte
│ │ ├── global.d.ts
│ │ ├── main.ts
│ │ ├── package.ts
│ │ ├── router.ts
│ │ └── views
│ │ │ ├── Dynamic.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── HomeTransition.svelte
│ │ │ ├── Null.svelte
│ │ │ ├── Profile.svelte
│ │ │ ├── ProfileBio.svelte
│ │ │ └── ProfileWelcome.svelte
│ └── tsconfig.json
└── tsconfig.json
├── docs
├── api.md
├── comparison.md
└── guide.md
├── examples
├── basic
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── rollup.config.js
│ └── src
│ │ ├── App.svelte
│ │ ├── main.js
│ │ ├── router.js
│ │ └── routes
│ │ ├── Home.svelte
│ │ └── User.svelte
├── lazy-loading
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── rollup.config.js
│ └── src
│ │ ├── App.svelte
│ │ ├── main.js
│ │ ├── router.js
│ │ └── routes
│ │ ├── Home.svelte
│ │ └── User.svelte
└── react-router
│ ├── README.md
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── rollup.config.js
│ └── src
│ ├── App.svelte
│ ├── main.js
│ ├── react
│ └── MyReactComponent.js
│ ├── router.js
│ └── routes
│ ├── Home.svelte
│ └── User.svelte
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── rollup.config.js
├── scripts
├── copy-svelte.js
└── cy-serve.js
├── src
├── components
│ ├── Link.svelte
│ └── RouterView.svelte
├── global.ts
├── index.ts
├── location-change-shim.ts
├── matcher.ts
├── router
│ ├── base.ts
│ ├── hash-router.ts
│ └── path-router.ts
├── types.ts
└── util.ts
├── svelte.config.js
├── tsconfig.base.json
└── tsconfig.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '**.md'
7 | - 'examples/**'
8 | pull_request:
9 | paths-ignore:
10 | - '**.md'
11 | - 'examples/**'
12 |
13 | jobs:
14 | test:
15 | name: Test
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 |
21 | - name: Install pnpm
22 | run: npm i -g pnpm@6
23 |
24 | - name: Setup node
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: 16
28 | cache: 'pnpm'
29 | cache-dependency-path: '**/pnpm-lock.yaml'
30 |
31 | - name: Cache Cypress
32 | uses: actions/cache@v2
33 | with:
34 | path: ~/.cache/Cypress
35 | key: cypress-cache-ubuntu-${{ hashFiles('**/pnpm-lock.yaml') }}
36 |
37 | - name: Install dependencies
38 | run: pnpm i
39 |
40 | - name: Lint
41 | run: pnpm lint
42 |
43 | - name: Unit test
44 | run: pnpm test:unit
45 |
46 | - name: End-to-end test
47 | uses: cypress-io/github-action@v2
48 | with:
49 | headless: true
50 | start: pnpm cy:setup
51 | wait-on: 'http://localhost:10003'
52 | install: false
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 |
56 | - name: Upload Cypress screenshots
57 | uses: actions/upload-artifact@v1
58 | if: failure()
59 | with:
60 | name: cypress-screenshots
61 | path: cypress/screenshots
62 |
63 | - name: Upload Cypress videos
64 | uses: actions/upload-artifact@v1
65 | if: failure()
66 | with:
67 | name: cypress-videos
68 | path: cypress/videos
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/dist
3 | **/build
4 | .vscode
5 | cypress/screenshots
6 | cypress/videos
7 | examples/**/package-lock.json
8 | examples/**/yarn.lock
9 | examples/**/pnpm-lock.yaml
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/dist
3 | **/build
4 | .vscode
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "proseWrap": "never",
6 | "plugins": ["./node_modules/prettier-plugin-svelte"]
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.5.0 - 2021-09-10
4 |
5 | ### Added
6 |
7 | - Use location API for hash navigation ([#15](https://github.com/bluwy/svelte-router/pull/15))
8 |
9 | ## 0.4.1 - 2021-01-31
10 |
11 | ### Fixed
12 |
13 | - Don't remount component when enter nested routes (again)
14 |
15 | ## 0.4.0 - 2021-01-30
16 |
17 | ### Fixed
18 |
19 | - Don't remount component when enter nested routes
20 |
21 | ## 0.3.2 - 2020-10-11
22 |
23 | ### Fixed
24 |
25 | - Keep `navigate` and `createLink` reference when initializing router
26 |
27 | ## 0.3.1 - 2020-09-22
28 |
29 | ### Added
30 |
31 | - Support component dynamic import
32 |
33 | ## 0.3.0 - 2020-09-04
34 |
35 | ### Added
36 |
37 | - Improve `component` typing
38 |
39 | ### Changed
40 |
41 | - `route` only update if url change
42 | - Replace `initRouter` with `initHashRouter` and `initPathRouter` to improve tree-shaking
43 |
44 | ### Removed
45 |
46 | - Remove `RedirectOption` type export
47 |
48 | ## 0.2.1 - 2020-09-01
49 |
50 | ### Fixed
51 |
52 | - ` ` will work properly with special key clicks, e.g. ctrl-click
53 |
54 | ## 0.2.0 - 2020-08-30
55 |
56 | ### Added
57 |
58 | - Add `createLink` function to enable custom link creation
59 | - Format URL on page load
60 |
61 | ### Changed
62 |
63 | - Rename `history` mode to `path`
64 | - All router options need to be defined
65 | - All public APIs, except for `route`, will now throw error if called before `initRouter`
66 | - `initRouter` will throw error if called more than once
67 | - `LocationInput`'s `path`, `search` and `hash` will treat empty string as undefined
68 |
69 | ### Removed
70 |
71 | - Remove `link` action, use `createLink` instead
72 |
73 | ### Fixed
74 |
75 | - Fix synchronous redirect rendering
76 |
77 | ## 0.1.2 - 2020-08-26
78 |
79 | ### Fixed
80 |
81 | - Route store is always defined even before `initRouter`
82 | - `navigate` will not throw error if called before `initRouter`
83 | - Fix base path detection
84 |
85 | ## 0.1.1 - 2020-08-25
86 |
87 | ### Fixed
88 |
89 | - Fix unexpected slot "default" warning
90 |
91 | ## 0.1.0 - 2020-08-25
92 |
93 | Initial release
94 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Svelte Router
2 |
3 | Thanks for your interest in making `svelte-router` better!
4 |
5 | ## Issues and Feature Requests
6 |
7 | [Open an issue](https://github.com/bluwy/svelte-router/issues/new/choose) for any bug reports, unexpected behaviors, or feature requests. If possible, provide a reproduction repo to help us in resolving the issues.
8 |
9 | ## Documentation Updates
10 |
11 | All are welcomed to improve the documentation. The docs are currently located in [`/docs`](./docs). Do note that the docs follow the [Vue Docs Writing Guide](https://v3.vuejs.org/guide/contributing/writing-guide.html).
12 |
13 | ## Code Contribution
14 |
15 | When sending a pull request, make sure the changes are properly formatted and tested so it can be merged. See the [code formatting](#code-formatting) and [testing](#testing) sections below for more information.
16 |
17 | Below is the general development workflow:
18 |
19 | ### Local Development
20 |
21 | 1. Clone the repo
22 | 2. Run `pnpm install` to install dependencies
23 | 3. Run `pnpm cy:setup` to manually test the router
24 |
25 | This will start up 3 servers at:
26 |
27 | - http://localhost:10001 - `hash` mode
28 | - http://localhost:10002 - `path` mode
29 | - http://localhost:10003 - `path` mode with base tag
30 |
31 | ### Code Formatting
32 |
33 | The library uses [Prettier](https://prettier.io/) to format the code, including HTML, JS, TS and Svelte files.
34 |
35 | Code will be auto-formatted per git commits using [husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged).
36 |
37 | > NOTE: Markdown files are temporarily not formatted until [prettier-plugin-svelte#129](https://github.com/sveltejs/prettier-plugin-svelte/issues/129) is fixed.
38 |
39 | ### Testing
40 |
41 | The library performs unit testing (with [Jest](https://jestjs.io)) and e2e testing (with [Cypress](https://cypress.io)).
42 |
43 | - Run `pnpm test:unit` for unit tests
44 | - Run `pnpm test:e2e` for e2e tests
45 |
46 | When pushing or sending a pull request to this repo, a GitHub Actions workflow will run both unit and e2e tests.
47 |
48 | ### Publishing
49 |
50 | There's currently no automated publishing workflow. For now, it is done locally by [bluwy](https://github.com/bluwy).
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Bjorn Lu
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte Router
2 |
3 |
4 | [](https://www.npmjs.com/package/@bjornlu/svelte-router)
5 | [](https://www.npmjs.com/package/@bjornlu/svelte-router)
6 | [](https://github.com/bluwy/svelte-router/actions)
7 |
8 | > Before you start, I'd highly recommend checking out [SvelteKit](https://github.com/sveltejs/kit) first. It has a flexible filesystem-based router officially supported by the Svelte team.
9 |
10 | An easy-to-use SPA router for Svelte.
11 |
12 | [**Comparison with other routers**](./docs/comparison.md)
13 |
14 | ## Features
15 |
16 | - Super simple API
17 | - Support `hash` and `path` based navigation
18 | - Nested routes
19 | - Redirects and navigation guards (with async support)
20 | - Lazy loading routes
21 | - Auto `base` tag navigation
22 |
23 | ## Not Supported
24 |
25 | - Server-side rendering (SSR) - Use [SvelteKit](https://github.com/sveltejs/kit) instead
26 | - Relative navigations - Use absolute path and [dynamic syntax](./docs/guide.md#dynamic-syntax) instead
27 |
28 | ## Quick Start
29 |
30 | Install [`@bjornlu/svelte-router`](https://www.npmjs.com/package/@bjornlu/svelte-router):
31 |
32 | ```bash
33 | $ npm install @bjornlu/svelte-router
34 | ```
35 |
36 | Define routes:
37 |
38 | ```js
39 | // main.js
40 |
41 | import { initPathRouter } from '@bjornlu/svelte-router'
42 | import App from './App.svelte'
43 | import Home from './Home.svelte'
44 |
45 | // Use `initHashRouter` for hash mode
46 | initPathRouter([{ path: '/', component: Home }])
47 |
48 | const app = new App({
49 | target: document.body
50 | })
51 |
52 | export default app
53 | ```
54 |
55 | Render routes and links:
56 |
57 | ```svelte
58 |
59 |
60 |
63 |
64 |
65 |
66 | Home
67 |
68 |
69 |
70 | ```
71 |
72 | Done!
73 |
74 | ## Documentation
75 |
76 | Ready to learn more? The documentation is split into two parts:
77 |
78 | - [Guide](./docs/guide.md): Covers common usage to advanced topics
79 | - [API reference](./docs/api.md): Covers the structure of the API
80 |
81 | ## Examples
82 |
83 | - [Basic](./examples/basic): Basic router usage
84 | - [Lazy loading](./examples/lazy-loading): Lazy loading setup example
85 |
86 | ## Contributing
87 |
88 | All contributions are welcomed. Check out [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
89 |
90 | ## License
91 |
92 | MIT
93 |
--------------------------------------------------------------------------------
/__tests__/init-usage.spec.ts:
--------------------------------------------------------------------------------
1 | import { initHashRouter, initPathRouter } from '../src'
2 |
3 | describe('init usage', () => {
4 | it('should throw if initRouter called more than once', () => {
5 | const call = () => {
6 | initHashRouter([])
7 | initPathRouter([])
8 | }
9 | expect(call).toThrow()
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/__tests__/matcher.spec.ts:
--------------------------------------------------------------------------------
1 | import { RouteMatcher } from '../src/matcher'
2 |
3 | describe('matcher', () => {
4 | it('should match a route', () => {
5 | const matcher = new RouteMatcher([{ path: '/foo' }, { path: '/bar' }])
6 | const route = matcher.matchRoute('/bar')
7 | expect(route?.matched[0].path).toBe('/bar')
8 | })
9 |
10 | it('should match the same sequence of route records', () => {
11 | const matcher = new RouteMatcher([
12 | {
13 | path: '/foo',
14 | children: [
15 | {
16 | path: '/bar',
17 | children: [{ path: '/baz' }]
18 | }
19 | ]
20 | }
21 | ])
22 | const route = matcher.matchRoute('/foo/bar/baz')
23 | expect(route?.matched.length).toBe(3)
24 | expect(route?.matched[0].path).toBe('/foo')
25 | expect(route?.matched[1].path).toBe('/bar')
26 | expect(route?.matched[2].path).toBe('/baz')
27 | })
28 |
29 | it('should match nested routes', () => {
30 | const matcher = new RouteMatcher([
31 | {
32 | path: '/foo',
33 | children: [{ path: '/bar' }]
34 | }
35 | ])
36 | const route1 = matcher.matchRoute('/foo/bar')
37 | const route2 = matcher.matchRoute('/foo')
38 | expect(route1?.matched[1].path).toBe('/bar')
39 | expect(route2?.matched.length).toBe(1)
40 | expect(route2?.matched[0].path).toBe('/foo')
41 | })
42 |
43 | it('should be tolerant to oddly named paths', () => {
44 | const matcher = new RouteMatcher([
45 | {
46 | path: 'foo',
47 | children: [{ path: 'bar/' }, { path: 'baz' }, { path: '*' }]
48 | }
49 | ])
50 | const route1 = matcher.matchRoute('/foo/bar')
51 | const route2 = matcher.matchRoute('/foo/baz')
52 | const route3 = matcher.matchRoute('/foo/bla/')
53 | expect(route1?.matched[1].path).toBe('bar/')
54 | expect(route2?.matched[1].path).toBe('baz')
55 | expect(route3?.params.wild).toBe('/bla')
56 | })
57 |
58 | it('should parse named parameters', () => {
59 | const matcher = new RouteMatcher([{ path: '/foo/:id' }])
60 | const route = matcher.matchRoute('/foo/abc')
61 | expect(route?.params.id).toBe('abc')
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/__tests__/pre-init-usage.spec.ts:
--------------------------------------------------------------------------------
1 | import { get } from 'svelte/store'
2 | import { route, navigate, createLink } from '../src'
3 |
4 | describe('API access before router initialization', function () {
5 | it('should have a default route store value', function () {
6 | const $route = get(route)
7 | expect(typeof $route === 'object').toBe(true)
8 | })
9 |
10 | it('should throw error if navigate', function () {
11 | expect(() => navigate(0)).toThrow()
12 | })
13 |
14 | it('should throw error if createLink', function () {
15 | expect(() => createLink({})).toThrow()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/__tests__/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["jest", "svelte"]
5 | },
6 | "include": ["**/*.spec.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectId": "vjxpm8",
3 | "videoUploadOnPasses": false
4 | }
5 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/integration/hash_router.spec.ts:
--------------------------------------------------------------------------------
1 | describe('hash router', { baseUrl: 'http://localhost:10001' }, () => {
2 | it('should render initial route', () => {
3 | cy.visit('#/')
4 | cy.get('h2').contains('Home').should('exist')
5 | })
6 |
7 | it('should change hash on route change', () => {
8 | cy.visit('#/')
9 | cy.get('a').contains('Null').click()
10 | cy.hash().should('eq', '#/null')
11 | cy.get('h2').contains('404').should('exist')
12 | })
13 |
14 | it('should handle nested routes', () => {
15 | cy.visit('#/profile/123')
16 | cy.get('h2').contains('Profile').should('exist')
17 | })
18 |
19 | it('should handle programmatic navigation', () => {
20 | cy.visit('#/')
21 | cy.get('#profile-id').type('123')
22 | cy.get('#profile-button').click()
23 | cy.contains('Your ID is 123').should('exist')
24 | cy.hash().should('eq', '#/profile/123/welcome')
25 | })
26 |
27 | it('should handle param shorthand paths', () => {
28 | cy.visit('#/profile/123')
29 | cy.get('a').contains('Welcome').click()
30 | cy.contains('Your ID is 123').should('exist')
31 | cy.hash().should('eq', '#/profile/123/welcome')
32 | })
33 |
34 | it('should display correct href for link component', () => {
35 | cy.visit('#/profile/123')
36 | cy.get('a')
37 | .contains('Welcome')
38 | .should('have.attr', 'href')
39 | .and('eq', '#/profile/123/welcome')
40 | })
41 |
42 | it('should redirect', () => {
43 | cy.visit('#/secret')
44 | cy.hash().should('eq', '#/')
45 | cy.get('h2').contains('Home').should('exist')
46 | })
47 |
48 | it('should not redirect if undefined', () => {
49 | cy.visit('#/profile/123/bio')
50 | cy.get('h3').contains('Bio').should('exist')
51 | })
52 |
53 | it('should dynamically import a route', () => {
54 | cy.visit('#/')
55 | cy.get('a').contains('Dynamic').click()
56 | cy.contains('Dynamic import').should('exist')
57 | })
58 |
59 | it('should handle nested route transitions', () => {
60 | cy.visit('#/')
61 | cy.get('a').contains('Transition').click()
62 | cy.contains('Transition test').should('exist')
63 | cy.get('a').contains('Home').click()
64 | cy.contains('Transition test').should('not.exist')
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/cypress/integration/history_base_router.spec.ts:
--------------------------------------------------------------------------------
1 | describe('history base router', { baseUrl: 'http://localhost:10003' }, () => {
2 | it('should render initial route', () => {
3 | cy.visit('/test')
4 | cy.get('h2').contains('Home').should('exist')
5 | })
6 |
7 | it('should change pathname on route change', () => {
8 | cy.visit('/test')
9 | cy.get('a').contains('Null').click()
10 | cy.location('pathname').should('eq', '/test/null')
11 | cy.get('h2').contains('404').should('exist')
12 | })
13 |
14 | it('should handle nested routes', () => {
15 | cy.visit('/test/profile/123')
16 | cy.get('h2').contains('Profile').should('exist')
17 | })
18 |
19 | it('should handle programmatic navigation', () => {
20 | cy.visit('/test')
21 | cy.get('#profile-id').type('123')
22 | cy.get('#profile-button').click()
23 | cy.contains('Your ID is 123').should('exist')
24 | cy.location('pathname').should('eq', '/test/profile/123/welcome')
25 | })
26 |
27 | it('should handle param shorthand paths', () => {
28 | cy.visit('/test/profile/123')
29 | cy.get('a').contains('Welcome').click()
30 | cy.contains('Your ID is 123').should('exist')
31 | cy.location('pathname').should('eq', '/test/profile/123/welcome')
32 | })
33 |
34 | it('should display correct href for link component', () => {
35 | cy.visit('/test/profile/123')
36 | cy.get('a')
37 | .contains('Welcome')
38 | .should('have.attr', 'href')
39 | .and('eq', '/test/profile/123/welcome')
40 | })
41 |
42 | it('should redirect', () => {
43 | cy.visit('/test/secret')
44 | cy.location('pathname').should('eq', '/test')
45 | cy.get('h2').contains('Home').should('exist')
46 | })
47 |
48 | it('should not redirect if undefined', () => {
49 | cy.visit('/test/profile/123/bio')
50 | cy.get('h3').contains('Bio').should('exist')
51 | })
52 |
53 | it('should dynamically import a route', () => {
54 | cy.visit('/test')
55 | cy.get('a').contains('Dynamic').click()
56 | cy.contains('Dynamic import').should('exist')
57 | })
58 |
59 | it('should handle nested route transitions', () => {
60 | cy.visit('/test')
61 | cy.get('a').contains('Transition').click()
62 | cy.contains('Transition test').should('exist')
63 | cy.get('a').contains('Home').click()
64 | cy.contains('Transition test').should('not.exist')
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/cypress/integration/history_router.spec.ts:
--------------------------------------------------------------------------------
1 | describe('history router', { baseUrl: 'http://localhost:10002' }, () => {
2 | it('should render initial route', () => {
3 | cy.visit('/')
4 | cy.get('h2').contains('Home').should('exist')
5 | })
6 |
7 | it('should change pathname on route change', () => {
8 | cy.visit('/')
9 | cy.get('a').contains('Null').click()
10 | cy.location('pathname').should('eq', '/null')
11 | cy.get('h2').contains('404').should('exist')
12 | })
13 |
14 | it('should handle nested routes', () => {
15 | cy.visit('/profile/123')
16 | cy.get('h2').contains('Profile').should('exist')
17 | })
18 |
19 | it('should handle programmatic navigation', () => {
20 | cy.visit('/')
21 | cy.get('#profile-id').type('123')
22 | cy.get('#profile-button').click()
23 | cy.contains('Your ID is 123').should('exist')
24 | cy.location('pathname').should('eq', '/profile/123/welcome')
25 | })
26 |
27 | it('should handle param shorthand paths', () => {
28 | cy.visit('/profile/123')
29 | cy.get('a').contains('Welcome').click()
30 | cy.contains('Your ID is 123').should('exist')
31 | cy.location('pathname').should('eq', '/profile/123/welcome')
32 | })
33 |
34 | it('should display correct href for link component', () => {
35 | cy.visit('/profile/123')
36 | cy.get('a')
37 | .contains('Welcome')
38 | .should('have.attr', 'href')
39 | .and('eq', '/profile/123/welcome')
40 | })
41 |
42 | it('should redirect', () => {
43 | cy.visit('/secret')
44 | cy.location('pathname').should('eq', '/')
45 | cy.get('h2').contains('Home').should('exist')
46 | })
47 |
48 | it('should not redirect if undefined', () => {
49 | cy.visit('/profile/123/bio')
50 | cy.get('h3').contains('Bio').should('exist')
51 | })
52 |
53 | it('should dynamically import a route', () => {
54 | cy.visit('/')
55 | cy.get('a').contains('Dynamic').click()
56 | cy.contains('Dynamic import').should('exist')
57 | })
58 |
59 | it('should handle nested route transitions', () => {
60 | cy.visit('/')
61 | cy.get('a').contains('Transition').click()
62 | cy.contains('Transition test').should('exist')
63 | cy.get('a').contains('Home').click()
64 | cy.contains('Transition test').should('not.exist')
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/cypress/test-app/public-hash/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router Hash Test
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/cypress/test-app/public-path-base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router Path Base Test
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/cypress/test-app/public-path/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router Path Test
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/cypress/test-app/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import replace from '@rollup/plugin-replace'
4 | import typescript from '@rollup/plugin-typescript'
5 | import svelte from 'rollup-plugin-svelte'
6 |
7 | const svelteConfig = require('../../svelte.config')
8 |
9 | const p = (...args) => path.resolve(__dirname, ...args)
10 |
11 | const createConfig = (routerMode, publicPath) => ({
12 | input: p('src/main.ts'),
13 | output: {
14 | sourcemap: true,
15 | dir: p(publicPath, 'build'),
16 | format: 'es',
17 | name: 'app'
18 | },
19 | plugins: [
20 | svelte({
21 | dev: true,
22 | preprocess: svelteConfig.preprocess,
23 | css: (v) => v.write('bundle.css')
24 | }),
25 | resolve(),
26 | replace({
27 | preventAssignment: true,
28 | values: {
29 | __ROUTER_MODE__: JSON.stringify(routerMode),
30 | 'process.env.NODE_ENV': JSON.stringify('development')
31 | }
32 | }),
33 | typescript({
34 | tsconfig: p('tsconfig.json')
35 | })
36 | ]
37 | })
38 |
39 | export default [
40 | createConfig('hash', 'public-hash'),
41 | createConfig('path', 'public-path'),
42 | createConfig('path', 'public-path-base')
43 | ]
44 |
--------------------------------------------------------------------------------
/cypress/test-app/src/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | Home
10 | Null
11 | Dynamic
12 | Transition
13 | Home replace
14 |
15 |
16 |
17 | navigate(`/profile/${profileId}/welcome`)}
20 | >
21 | Login
22 |
23 |
24 |
Svelte Routing Test
25 |
26 |
27 |
--------------------------------------------------------------------------------
/cypress/test-app/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const __ROUTER_MODE__: 'hash' | 'path'
2 |
--------------------------------------------------------------------------------
/cypress/test-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 | import './router'
3 |
4 | const app = new App({
5 | target: document.getElementById('app') as Element
6 | })
7 |
8 | export default app
9 |
--------------------------------------------------------------------------------
/cypress/test-app/src/package.ts:
--------------------------------------------------------------------------------
1 | // Re-export for convenience
2 | export * from '../../../src'
3 |
--------------------------------------------------------------------------------
/cypress/test-app/src/router.ts:
--------------------------------------------------------------------------------
1 | import { initHashRouter, initPathRouter } from './package'
2 | import Home from './views/Home.svelte'
3 | import HomeTransition from './views/HomeTransition.svelte'
4 | import Profile from './views/Profile.svelte'
5 | import ProfileWelcome from './views/ProfileWelcome.svelte'
6 | import ProfileBio from './views/ProfileBio.svelte'
7 | import Null from './views/Null.svelte'
8 |
9 | const initRouter = __ROUTER_MODE__ === 'hash' ? initHashRouter : initPathRouter
10 |
11 | initRouter([
12 | {
13 | path: '/',
14 | component: Home,
15 | children: [
16 | {
17 | path: '/transition',
18 | component: HomeTransition
19 | }
20 | ]
21 | },
22 | {
23 | path: '/profile/:id',
24 | component: Profile,
25 | children: [
26 | {
27 | path: '/welcome',
28 | component: ProfileWelcome
29 | },
30 | {
31 | path: '/bio',
32 | component: ProfileBio,
33 | redirect: () =>
34 | new Promise((resolve) => {
35 | setTimeout(() => resolve(undefined), 1000)
36 | })
37 | }
38 | ]
39 | },
40 | {
41 | path: '/secret',
42 | redirect: '/'
43 | },
44 | {
45 | path: '/dynamic',
46 | component: () => import('./views/Dynamic.svelte')
47 | },
48 | {
49 | path: '/*',
50 | component: Null
51 | }
52 | ])
53 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/Dynamic.svelte:
--------------------------------------------------------------------------------
1 | Dynamic import
2 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/Home.svelte:
--------------------------------------------------------------------------------
1 | Home
2 |
3 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/HomeTransition.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | Transition test
6 |
7 |
12 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/Null.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
404
7 |
8 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/Profile.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | Profile
8 | Your ID is {id}
9 |
10 |
11 | Welcome
12 | Bio
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/ProfileBio.svelte:
--------------------------------------------------------------------------------
1 | Bio
2 |
--------------------------------------------------------------------------------
/cypress/test-app/src/views/ProfileWelcome.svelte:
--------------------------------------------------------------------------------
1 | Welcome
2 |
--------------------------------------------------------------------------------
/cypress/test-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "ES2020"
5 | },
6 | "include": ["**/*.ts", "../../src"],
7 | "exclude": ["**/build"]
8 | }
9 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "baseUrl": "../node_modules",
5 | "target": "es5",
6 | "lib": ["es5", "dom"],
7 | "types": ["cypress"]
8 | },
9 | "include": ["**/*.ts"],
10 | "exclude": ["test-app"]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | ## Table of Contents
4 |
5 | - [` `](#routerview-)
6 | - [` `](#link-)
7 | - [`initHashRouter(routes: RouteRecord[])`](#inithashrouterroutes-routerecord)
8 | - [`initPathRouter(routes: RouteRecord[])`](#initpathrouterroutes-routerecord)
9 | - [`navigate()`](#navigate)
10 | - [`navigate(to: number)`](#navigateto-number)
11 | - [`navigate(to: string | LocationInput, replace?: boolean)`](#navigateto-string--locationinput-replace-boolean)
12 | - [`$route`](#route)
13 | - [`createLink(to: string | LocationInput)`](#createlinkto-string--locationinput)
14 |
15 | ## ` `
16 |
17 | Renders the routes. No additional props.
18 |
19 | ## ` `
20 |
21 |
22 | | Prop | Type | Default | Description |
23 | |---------|-------------------------|---------|------------------------------------------|
24 | | to | string \| LocationInput | | Target route |
25 | | replace | boolean | `false` | Replace current route instead of pushing |
26 |
27 | - Renders an anchor tag
28 | - Adds `aria-current="page"` when link exactly matches
29 | - Display correct `href` with resolved dynamic segments, base path, and hash prepends, e.g. `/user/:id` => `#/user/foo`
30 | - Adds active class names based on `to` and current route path:
31 | - `link-active` when link partially matches, e.g. `/foo` matches `/foo/bar`
32 | - `link-exact-active` when link exactly matches, e.g. `/foo` matches `/foo`
33 |
34 | ## `initHashRouter(routes: RouteRecord[])`
35 |
36 | Initializes a `hash` mode router for the app.
37 |
38 | ## `initPathRouter(routes: RouteRecord[])`
39 |
40 | Initializes a `path` mode router for the app.
41 |
42 | ## `navigate()`
43 |
44 | `navigate()` has two function signatures:
45 |
46 | ### `navigate(to: number)`
47 |
48 | Navigate using an offset in the current history. Works the same way as [`history.go`](https://developer.mozilla.org/en-US/docs/Web/API/History/go).
49 |
50 | ### `navigate(to: string | LocationInput, replace?: boolean)`
51 |
52 | Navigate to a route using a path string or a location descriptor object. `string` and `LocationInput` are semantically equal and are just different ways to express the same route. For example:
53 |
54 | ```js
55 | '/foo?key=value#top'
56 |
57 | // same as
58 |
59 | {
60 | path: '/foo',
61 | search: { key: 'value' },
62 | hash: '#top'
63 | }
64 | ```
65 |
66 | ## `$route`
67 |
68 | A readable store that contains the current route information.
69 |
70 |
71 | | Property | Type | Example | Description |
72 | |----------|------------------------|-----------------|-----------------------------------------------------------|
73 | | path | string | `'/user/foo'` | The route path without search and hash |
74 | | params | Record | `{ id: 'foo' }` | The parsed dynamic segments of the path, e.g. `/user/:id` |
75 | | search | URLSearchParams | | The path search parsed with URLSearchParams |
76 | | hash | string | `'#hey'` | The path hash with leading `#`. Empty string if no hash. |
77 | | matched | RouteRecord[] | | The array of route records that matches the current route |
78 |
79 | ## `createLink(to: string | LocationInput)`
80 |
81 | Returns a readable store that contains the given link's information.
82 |
83 |
84 | | Property | Type | Example | Description |
85 | |---------------|---------|----------------|-----------------------------------------------------------------------|
86 | | href | string | `'#/user/foo'` | The path with resolved dynamic segments, base path, and hash prepends |
87 | | isActive | boolean | `false` | Whether the link is partially matching the current path |
88 | | isExactActive | boolean | `false` | Whether the link is exactly matching the current path |
89 |
--------------------------------------------------------------------------------
/docs/comparison.md:
--------------------------------------------------------------------------------
1 | # Comparison
2 |
3 | _Last updated: 22 September 2020_
4 |
5 | ## Key Difference
6 |
7 | One main feature that most routers don't have is a global route store, that is information about the current `path`, `search`, `hash`, and `params`. Most routers pass this data via props or via [context](https://svelte.dev/tutorial/context-api) which cannot be used outside of components.
8 |
9 | Besides that, `svelte-router` also has a unique [dynamic syntax](./guide.md#dynamic-syntax), which makes it clear where a link navigates to. For example, `navigate('/user/:id/profile')` instead of `navigate('../../profile')`.
10 |
11 | For a more fine-grained control, `svelte-router` exposes [`createLink()`](./guide.md#link-information), which is a nice low-level utility to retrieve link information without actually creating one.
12 |
13 | ## General Difference
14 |
15 | A non-exhaustive list of differences between popular routers:
16 |
17 |
18 | | | [@bjornlu/svelte-router](https://github.com/bluwy/svelte-router) | [svelte-routing](https://github.com/EmilTholin/svelte-routing) | [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) | [svelte-router-spa](https://github.com/jorgegorka/svelte-router) | [routify](https://github.com/roxiness/routify) |
19 | |---------------------------------|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
20 | | Route definition style | JS object | Component | JS object. Nested routes are separated into different objects. | JS object | File-system |
21 | | Router mode | Hash and path | Path | Hash | Path | Hash and path |
22 | | Access route data | Access global route store anywhere | Passed by prop and `let:param`. [A workaround](https://github.com/EmilTholin/svelte-routing/issues/41#issuecomment-503462045) can be used for descendant components. | `params` is passed by props. `path` and `search` can be accessed anywhere. | Passed by props only | Exposes global [search](https://routify.dev/docs/helpers#params) store. [`path`]((https://routify.dev/docs/helpers#url)) and [`params`](https://routify.dev/docs/helpers#params) are passed with context. |
23 | | Redirects and navigation guards | Built-in with async support | Manual redirect in components | Built-in with async support | Built-in but [no async support](https://github.com/jorgegorka/svelte-router/issues/20) | Manual redirect in components |
24 | | SSR support | :x: | :heavy_check_mark: | :x: | :x: | :x: |
25 | | Relative navigation | :x: | :x: | :x: | :x: | :heavy_check_mark: |
26 |
27 | ## Honorable Mentions
28 |
29 | Table so large, there's no space for these great routers:
30 |
31 | - [yrv](https://github.com/pateketrueke/yrv)
32 | - [svelte-navigator](https://github.com/mefechoel/svelte-navigator)
33 | - [tinro](https://github.com/AlexxNB/tinro)
34 |
--------------------------------------------------------------------------------
/docs/guide.md:
--------------------------------------------------------------------------------
1 | # Guide
2 |
3 | Here contains all you need to know about `svelte-router`!
4 |
5 | If you haven't setup `svelte-router`, check out the [quick start](../README.md#quick-start)!
6 |
7 | ## Table of Contents
8 |
9 | - [Router Modes](#router-modes)
10 | - [Dynamic Route Matching](#dynamic-route-matching)
11 | - [Catch-all Route](#catch-all-route)
12 | - [Matching Priority](#matching-priority)
13 | - [Nested Routes](#nested-routes)
14 | - [Route Navigation](#route-navigation)
15 | - [Navigate with Links](#navigate-with-links)
16 | - [Programmatic Navigation](#programmatic-navigation)
17 | - [Dynamic Syntax](#dynamic-syntax)
18 | - [Route Information](#route-information)
19 | - [Link Information](#link-information)
20 | - [Redirects and Navigation Guards](#redirects-and-navigation-guards)
21 | - [Lazy Loading Routes](#lazy-loading-routes)
22 | - [Transitions](#transitions)
23 | - [Base Path](#base-path)
24 |
25 | ## Router Modes
26 |
27 | `svelte-router` supports both `hash` and `path` mode routing. `hash` mode works by manipulating the URL hash, while `path` mode works by manipulating the URL path:
28 |
29 |
30 | | Mode | Initialize API | Example URL |
31 | |------|--------------------|--------------------------------|
32 | | hash | `initHashRouter()` | https://example.com/#/user/foo |
33 | | path | `initPathRouter()` | https://example.com/user/foo |
34 |
35 | Do note that `path` mode requires additional configuration on the server-side to serve a single HTML file for all URL paths. For example, the server should serve http://example.com/index.html when the user visits https://example.com/user/foo.
36 |
37 | ## Dynamic Route Matching
38 |
39 | Sometimes a route path cannot be known ahead of time. For example, a user ID may be used in the route to show its corresponding information.
40 |
41 | In `svelte-router`, we can use a dynamic segment to capture the variable:
42 |
43 | ```js
44 | [
45 | // Dynamic segment starts with ":" and the following text is its variable name
46 | { path: '/user/:id', component: User }
47 | ]
48 | ```
49 |
50 | Now when the user visits `/user/foo`, it will match and render the `User` component. The `id` variable can then be accessed through `$route.params`:
51 |
52 | ```svelte
53 |
54 |
55 |
60 | ```
61 |
62 | ### Catch-all Route
63 |
64 | When no routes can be matched, we can use a wildcard as a safety net to render a fallback component:
65 |
66 | ```js
67 | [
68 | { path: '/user', component: User },
69 | // Render fallback if route isn't `/user`
70 | { path: '/*', component: Fallback }
71 | ]
72 | ```
73 |
74 | When the catch-all route is matched, `$route.params.wild` will also return the path that matched the wildcard. For example when navigating to `/non-exist`, `$route.params.wild` returns `/non-exist`.
75 |
76 | ### Matching Priority
77 |
78 | `svelte-router` matches the defined routes sequentially from the first to last. For example:
79 |
80 | ```js
81 | [
82 | { path: '/:foo', component: Foo },
83 | { path: '/user', component: User }
84 | ]
85 | ```
86 |
87 | When the URL is `/user`, it will match `/:foo` instead since it satisfies the pattern first. Hence, routes with higher specificity should be placed before routes with lower specificity, such as dynamic segments and wildcards.
88 |
89 | ## Nested Routes
90 |
91 | Similar to the hierarchical nature of DOM elements, segments of the URL path may also represent the hierarchy of the routes and its components.
92 |
93 | In `svelte-router`, nested routes can be configured with the `children` property:
94 |
95 | ```js
96 | [
97 | {
98 | path: '/user/:id',
99 | component: User,
100 | children: [
101 | { path: '/profile', component: UserProfile },
102 | { path: '/posts', component: UserPosts }
103 | ]
104 | }
105 | ]
106 | ```
107 |
108 | For the `User` component to render its children, a ` ` tag must be present:
109 |
110 | ```svelte
111 |
112 |
113 | User
114 |
115 |
Children
116 |
117 |
118 | ```
119 |
120 | Now when the user visits `/user/foo/profile`, it will render `User` and `UserProfile`.
121 |
122 | ## Route Navigation
123 |
124 | `svelte-router` supports both declarative and programmatic ways of navigating between routes, that is with the ` ` component and the `navigate()` function.
125 |
126 | ### Navigate with Links
127 |
128 | The ` ` component has a `to` prop which accepts either a string path or location descriptor object. For example:
129 |
130 | ```svelte
131 |
134 |
135 |
136 | ...
137 |
138 |
139 | ...
140 |
141 |
142 | ...
143 |
144 |
145 | ...
146 | ```
147 |
148 | ### Programmatic Navigation
149 |
150 | Sometimes manual navigation is needed to respond to a user action, like a form submission. The `navigate()` function can be used to do so:
151 |
152 | ```js
153 | import { navigate } from '@bjornlu/svelte-router'
154 |
155 | // Navigate to `/`
156 | navigate('/')
157 |
158 | // Navigate to `/login?foo=bar`
159 | navigate('/login?foo=bar')
160 |
161 | // Navigate to `/login?foo=bar`
162 | navigate({ path: '/login', search: { foo: 'bar' } })
163 |
164 | // Navigate to `/` with replace
165 | navigate('/', true)
166 |
167 | // Navigate forward
168 | navigate(1)
169 |
170 | // Navigate backward
171 | navigate(-1)
172 | ```
173 |
174 | ### Dynamic Syntax
175 |
176 | `svelte-router` supports defining dynamic segments in the string path and location descriptor object. This makes it convenient to navigate in deeply nested routes instead of relying on relative navigation. For example:
177 |
178 | ```svelte
179 |
184 |
185 |
196 |
197 |
198 | ...
199 |
200 |
201 | ...
202 | ```
203 |
204 | Do note that:
205 |
206 | - In `hash` mode, the syntax will not work in the `search` portion of the location
207 | - In `path` mode, the syntax will not work in both the `search` and `hash` portion of the location
208 |
209 | ## Route Information
210 |
211 | Very often the code needs to receive information about the current route to perform certain tasks.
212 |
213 | You can use the global `$route` to do so by subscribing to it anywhere in your app:
214 |
215 | ```svelte
216 |
221 | ```
222 |
223 | The `$route` contains properties such as `path`, `search`, `hash`, `params`, and `matched`. You can learn more about it in its [API reference](./api.md/#route).
224 |
225 | ## Link Information
226 |
227 | To find out if a link is partially or exactly matching the current path, `svelte-router` provides a `createLink()` function to easily retrieve this information. For example:
228 |
229 | ```svelte
230 |
236 |
237 |
249 | ```
250 |
251 | Internally, the ` ` component also uses this [under-the-hood](https://github.com/bluwy/svelte-router/blob/master/src/components/Link.svelte), which means you can also use `createLink()` to create your own custom links!
252 |
253 | ## Redirects and Navigation Guards
254 |
255 | When navigating to certain invalid or protected routes, a redirect may be required.
256 |
257 | In `svelte-router`, the route `redirect` property can be used to acheive that:
258 |
259 | ```js
260 | [
261 | // If path is `/secret`, redirect to `/login`
262 | { path: '/secret', redirect: '/login' }
263 | ]
264 | ```
265 |
266 | Besides that, `redirect` also accepts a function or asynchronous function that may return the redirect path. This can be used to decide which path to redirect to depending on the condition. For example:
267 |
268 | ```js
269 | [
270 | {
271 | path: '/secret',
272 | redirect: async () => {
273 | // Call external function to check if user is authenticated
274 | if (await isAuthenticated()) {
275 | // User is authenticated, stay on route
276 | return undefined
277 | } else {
278 | // User is not authenticated, redirect to `/`
279 | return '/'
280 | }
281 | }
282 | }
283 | ]
284 | ```
285 |
286 | ## Lazy Loading Routes
287 |
288 | In large applications, it may be necessary to split up components to reduce the initial download size. This is done through dynamic importing components so that bundlers like [Rollup](https://rollupjs.org/) and [Webpack](https://webpack.js.org/) can perform code-splitting on them and only load when needed.
289 |
290 | Lazy loading works by using the [dynamic import syntax](https://github.com/tc39/proposal-dynamic-import):
291 |
292 | ```js
293 | [
294 | { path: '/foo', component: import('./Foo.svelte') },
295 | { path: '/bar', component: () => import('./Bar.svelte') }
296 | ]
297 | ```
298 |
299 | ## Transitions
300 |
301 | Route components can use [svelte/transition](https://svelte.dev/tutorial/transition) to animate between routes. Wrap the route component with an element and apply the transition:
302 |
303 | ```svelte
304 |
307 |
308 |
309 | Content
310 |
311 | ```
312 |
313 | When navigating to another route, if the current route component's descendants transition too, it may cause the UI to be in a half-transitioned state. There are 2 solutions:
314 |
315 | 1. Use [local transitions](https://svelte.dev/tutorial/local-transitions): Make sure the descendants' transitions are local, e.g. `
`
316 |
317 | 2. [Tick](https://svelte.dev/tutorial/tick) before navigate: This will wait for the descendants' transitions to end before navigating, e.g. `tick().then(() => navigate('/foo'))`
318 |
319 | > NOTE: Only apply the solutions above if the issue arises.
320 |
321 | ## Base Path
322 |
323 | In `path` mode, if the app is served under a specific path of a domain, then a ` ` tag needs to be declared in the ` `. For example, if the app is hosted at `/app`:
324 |
325 | ```html
326 |
327 |
328 |
329 |
330 | ```
331 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | A basic example
4 |
5 | ## Try it out
6 |
7 | Copy example project with [degit](https://github.com/Rich-Harris/degit):
8 |
9 | ```bash
10 | $ npx degit bluwy/svelte-router/examples/basic svelte-app
11 | ```
12 |
13 | Install dependencies:
14 |
15 | ```bash
16 | $ cd svelte-app
17 | $ npm install
18 | ```
19 |
20 | Run development server:
21 |
22 | ```bash
23 | $ npm run dev
24 | ```
25 |
26 | Then, navigate to http://localhost:5000. You should see your app running. Edit a component file in `src`, save it, and the page will auto reload.
27 |
28 | To run in production:
29 |
30 | ```bash
31 | $ npm run build
32 | $ npm run start
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -cw",
8 | "start": "sirv public -s"
9 | },
10 | "devDependencies": {
11 | "@bjornlu/svelte-router": "^0.4.1",
12 | "@rollup/plugin-commonjs": "^14.0.0",
13 | "@rollup/plugin-node-resolve": "^8.0.0",
14 | "rollup": "^2.3.4",
15 | "rollup-plugin-livereload": "^2.0.0",
16 | "rollup-plugin-svelte": "^6.0.0",
17 | "rollup-plugin-terser": "^7.0.0",
18 | "svelte": "^3.0.0"
19 | },
20 | "dependencies": {
21 | "sirv-cli": "^1.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/basic/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router - Basic
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/basic/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import commonjs from '@rollup/plugin-commonjs'
4 | import livereload from 'rollup-plugin-livereload'
5 | import { terser } from 'rollup-plugin-terser'
6 |
7 | const production = !process.env.ROLLUP_WATCH
8 |
9 | function serve() {
10 | let server
11 |
12 | function toExit() {
13 | if (server) server.kill(0)
14 | }
15 |
16 | return {
17 | writeBundle() {
18 | if (server) return
19 | server = require('child_process').spawn(
20 | 'npm',
21 | ['run', 'start', '--', '--dev'],
22 | {
23 | stdio: ['ignore', 'inherit', 'inherit'],
24 | shell: true
25 | }
26 | )
27 |
28 | process.on('SIGTERM', toExit)
29 | process.on('exit', toExit)
30 | }
31 | }
32 | }
33 |
34 | export default {
35 | input: 'src/main.js',
36 | output: {
37 | sourcemap: true,
38 | format: 'iife',
39 | name: 'app',
40 | file: 'public/build/bundle.js'
41 | },
42 | plugins: [
43 | svelte({
44 | // enable run-time checks when not in production
45 | dev: !production,
46 | // we'll extract any component CSS out into
47 | // a separate file - better for performance
48 | css: (css) => {
49 | css.write('bundle.css')
50 | }
51 | }),
52 |
53 | // If you have external dependencies installed from
54 | // npm, you'll most likely need these plugins. In
55 | // some cases you'll need additional configuration -
56 | // consult the documentation for details:
57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
58 | resolve({
59 | browser: true,
60 | dedupe: ['svelte']
61 | }),
62 | commonjs(),
63 |
64 | // In dev mode, call `npm run start` once
65 | // the bundle has been generated
66 | !production && serve(),
67 |
68 | // Watch the `public` directory and refresh the
69 | // browser on changes when not in production
70 | !production && livereload('public'),
71 |
72 | // If we're building for production (npm run build
73 | // instead of npm run dev), minify
74 | production && terser()
75 | ],
76 | watch: {
77 | clearScreen: false
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/examples/basic/src/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Home
8 | User
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/basic/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 | import './router'
3 |
4 | const app = new App({
5 | target: document.body
6 | })
7 |
8 | export default app
9 |
--------------------------------------------------------------------------------
/examples/basic/src/router.js:
--------------------------------------------------------------------------------
1 | import { initPathRouter } from '@bjornlu/svelte-router'
2 | import Home from './routes/Home.svelte'
3 | import User from './routes/User.svelte'
4 |
5 | // Use `initHashRouter` for hash mode
6 | initPathRouter([
7 | { path: '/', component: Home },
8 | { path: '/user/:id', component: User },
9 | { path: '/secret', redirect: '/' }
10 | ])
11 |
--------------------------------------------------------------------------------
/examples/basic/src/routes/Home.svelte:
--------------------------------------------------------------------------------
1 | Home
2 | This is a home page
3 |
--------------------------------------------------------------------------------
/examples/basic/src/routes/User.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | User
6 | Hello, your user ID is "{$route.params.id}"
7 |
--------------------------------------------------------------------------------
/examples/lazy-loading/README.md:
--------------------------------------------------------------------------------
1 | # Lazy Loading
2 |
3 | An example with lazy loading components. There are many ways to support dynamic imports, this examples uses ``.
4 |
5 | ## Try it out
6 |
7 | Copy example project with [degit](https://github.com/Rich-Harris/degit):
8 |
9 | ```bash
10 | $ npx degit bluwy/svelte-router/examples/lazy-loading svelte-app
11 | ```
12 |
13 | Install dependencies:
14 |
15 | ```bash
16 | $ cd svelte-app
17 | $ npm install
18 | ```
19 |
20 | Run development server:
21 |
22 | ```bash
23 | $ npm run dev
24 | ```
25 |
26 | Then, navigate to http://localhost:5000. You should see your app running. Edit a component file in `src`, save it, and the page will auto reload.
27 |
28 | To run in production:
29 |
30 | ```bash
31 | $ npm run build
32 | $ npm run start
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/lazy-loading/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazy-loading",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -cw",
8 | "start": "sirv public -s"
9 | },
10 | "devDependencies": {
11 | "@bjornlu/svelte-router": "^0.4.1",
12 | "@rollup/plugin-commonjs": "^14.0.0",
13 | "@rollup/plugin-node-resolve": "^8.0.0",
14 | "rollup": "^2.3.4",
15 | "rollup-plugin-livereload": "^2.0.0",
16 | "rollup-plugin-svelte": "^6.0.0",
17 | "rollup-plugin-terser": "^7.0.0",
18 | "svelte": "^3.0.0"
19 | },
20 | "dependencies": {
21 | "sirv-cli": "^1.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/lazy-loading/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router - Lazy Loading
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/lazy-loading/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import commonjs from '@rollup/plugin-commonjs'
4 | import livereload from 'rollup-plugin-livereload'
5 | import { terser } from 'rollup-plugin-terser'
6 |
7 | const production = !process.env.ROLLUP_WATCH
8 |
9 | function serve() {
10 | let server
11 |
12 | function toExit() {
13 | if (server) server.kill(0)
14 | }
15 |
16 | return {
17 | writeBundle() {
18 | if (server) return
19 | server = require('child_process').spawn(
20 | 'npm',
21 | ['run', 'start', '--', '--dev'],
22 | {
23 | stdio: ['ignore', 'inherit', 'inherit'],
24 | shell: true
25 | }
26 | )
27 |
28 | process.on('SIGTERM', toExit)
29 | process.on('exit', toExit)
30 | }
31 | }
32 | }
33 |
34 | export default {
35 | input: 'src/main.js',
36 | output: {
37 | sourcemap: true,
38 | format: 'es',
39 | name: 'app',
40 | dir: 'public/build'
41 | },
42 | plugins: [
43 | svelte({
44 | // enable run-time checks when not in production
45 | dev: !production,
46 | // we'll extract any component CSS out into
47 | // a separate file - better for performance
48 | css: (css) => {
49 | css.write('bundle.css')
50 | }
51 | }),
52 |
53 | // If you have external dependencies installed from
54 | // npm, you'll most likely need these plugins. In
55 | // some cases you'll need additional configuration -
56 | // consult the documentation for details:
57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
58 | resolve({
59 | browser: true,
60 | dedupe: ['svelte']
61 | }),
62 | commonjs(),
63 |
64 | // In dev mode, call `npm run start` once
65 | // the bundle has been generated
66 | !production && serve(),
67 |
68 | // Watch the `public` directory and refresh the
69 | // browser on changes when not in production
70 | !production && livereload('public'),
71 |
72 | // If we're building for production (npm run build
73 | // instead of npm run dev), minify
74 | production && terser()
75 | ],
76 | watch: {
77 | clearScreen: false
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/examples/lazy-loading/src/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Home
8 | User
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/lazy-loading/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 | import './router'
3 |
4 | const app = new App({
5 | target: document.body
6 | })
7 |
8 | export default app
9 |
--------------------------------------------------------------------------------
/examples/lazy-loading/src/router.js:
--------------------------------------------------------------------------------
1 | import { initPathRouter } from '@bjornlu/svelte-router'
2 | import Home from './routes/Home.svelte'
3 |
4 | // Use `initHashRouter` for hash mode
5 | initPathRouter([
6 | { path: '/', component: Home },
7 | { path: '/user/:id', component: () => import('./routes/User.svelte') },
8 | { path: '/secret', redirect: '/' }
9 | ])
10 |
--------------------------------------------------------------------------------
/examples/lazy-loading/src/routes/Home.svelte:
--------------------------------------------------------------------------------
1 | Home
2 | This is a home page
3 |
--------------------------------------------------------------------------------
/examples/lazy-loading/src/routes/User.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | User
6 | Hello, your user ID is "{$route.params.id}"
7 |
--------------------------------------------------------------------------------
/examples/react-router/README.md:
--------------------------------------------------------------------------------
1 | # React router
2 |
3 | This example shows interoperability with react router.
4 |
5 | Note that only hash navigation is supported. See [issue #14](https://github.com/bluwy/svelte-router/issues/14) for details.
6 |
7 | ## Try it out
8 |
9 | Copy example project with [degit](https://github.com/Rich-Harris/degit):
10 |
11 | ```bash
12 | $ npx degit bluwy/svelte-router/examples/react-router svelte-app
13 | ```
14 |
15 | Install dependencies:
16 |
17 | ```bash
18 | $ cd svelte-app
19 | $ npm install
20 | ```
21 |
22 | Run development server:
23 |
24 | ```bash
25 | $ npm run dev
26 | ```
27 |
28 | Then, navigate to http://localhost:5000. You should see your app running. Edit a component file in `src`, save it, and the page will auto reload.
29 |
30 | To run in production:
31 |
32 | ```bash
33 | $ npm run build
34 | $ npm run start
35 | ```
36 |
--------------------------------------------------------------------------------
/examples/react-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -cw",
8 | "start": "sirv public -s"
9 | },
10 | "devDependencies": {
11 | "@bjornlu/svelte-router": "^0.4.1",
12 | "@rollup/plugin-commonjs": "^14.0.0",
13 | "@rollup/plugin-node-resolve": "^8.0.0",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2",
16 | "react-router-dom": "^5.3.0",
17 | "rollup": "^2.3.4",
18 | "rollup-plugin-livereload": "^2.0.0",
19 | "rollup-plugin-node-builtins": "^2.1.2",
20 | "rollup-plugin-node-globals": "^1.4.0",
21 | "rollup-plugin-svelte": "^6.0.0",
22 | "rollup-plugin-terser": "^7.0.0",
23 | "svelte": "^3.0.0"
24 | },
25 | "dependencies": {
26 | "sirv-cli": "^1.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/react-router/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Router - React Router
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/react-router/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import commonjs from '@rollup/plugin-commonjs'
4 | import livereload from 'rollup-plugin-livereload'
5 | import { terser } from 'rollup-plugin-terser'
6 | import globals from 'rollup-plugin-node-globals'
7 | import builtins from 'rollup-plugin-node-builtins'
8 |
9 | const production = !process.env.ROLLUP_WATCH
10 |
11 | function serve() {
12 | let server
13 |
14 | function toExit() {
15 | if (server) server.kill(0)
16 | }
17 |
18 | return {
19 | writeBundle() {
20 | if (server) return
21 | server = require('child_process').spawn(
22 | 'npm',
23 | ['run', 'start', '--', '--dev'],
24 | {
25 | stdio: ['ignore', 'inherit', 'inherit'],
26 | shell: true
27 | }
28 | )
29 |
30 | process.on('SIGTERM', toExit)
31 | process.on('exit', toExit)
32 | }
33 | }
34 | }
35 |
36 | export default {
37 | input: 'src/main.js',
38 | output: {
39 | sourcemap: true,
40 | format: 'iife',
41 | name: 'app',
42 | file: 'public/build/bundle.js'
43 | },
44 | plugins: [
45 | svelte({
46 | // enable run-time checks when not in production
47 | dev: !production,
48 | // we'll extract any component CSS out into
49 | // a separate file - better for performance
50 | css: (css) => {
51 | css.write('bundle.css')
52 | }
53 | }),
54 |
55 | // If you have external dependencies installed from
56 | // npm, you'll most likely need these plugins. In
57 | // some cases you'll need additional configuration -
58 | // consult the documentation for details:
59 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
60 | resolve({
61 | browser: true,
62 | dedupe: ['svelte']
63 | }),
64 |
65 | commonjs(),
66 | globals(),
67 | builtins(),
68 |
69 | // In dev mode, call `npm run start` once
70 | // the bundle has been generated
71 | !production && serve(),
72 |
73 | // Watch the `public` directory and refresh the
74 | // browser on changes when not in production
75 | !production && livereload('public'),
76 |
77 | // If we're building for production (npm run build
78 | // instead of npm run dev), minify
79 | production && terser()
80 | ],
81 | watch: {
82 | clearScreen: false
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/examples/react-router/src/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | This is the Svelte component with svelte-router.
7 |
8 | Home
9 | Home with details
10 | User
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/react-router/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte'
2 | import './router'
3 |
4 | const app = new App({
5 | target: document.body
6 | })
7 |
8 | export default app
9 |
--------------------------------------------------------------------------------
/examples/react-router/src/react/MyReactComponent.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import React from 'react'
3 | import { HashRouter as Router, Switch, Route, Link } from 'react-router-dom'
4 |
5 | export function MyReactComponent() {
6 | return React.createElement(
7 | Router,
8 | null,
9 | React.createElement(
10 | 'div',
11 | null,
12 | 'This is a react component with react-router',
13 | React.createElement('br', null),
14 | React.createElement(Link, { to: '/user/alain' }, 'Go to user'),
15 | ' ',
16 | React.createElement(Link, { to: '/home' }, 'Hide details'),
17 | ' ',
18 | React.createElement(Link, { to: '/home/details' }, 'Show details'),
19 | React.createElement('br', null),
20 | React.createElement(
21 | Switch,
22 | null,
23 | React.createElement(
24 | Route,
25 | { path: '/home/details' },
26 | 'Here are the details'
27 | )
28 | )
29 | )
30 | )
31 | }
32 |
33 | class MyReactElement extends HTMLElement {
34 | connectedCallback() {
35 | const mountPoint = document.createElement('span')
36 | this.attachShadow({ mode: 'open' }).appendChild(mountPoint)
37 | ReactDOM.render(
38 | React.createElement(MyReactComponent, null, null),
39 | mountPoint
40 | )
41 | }
42 | }
43 | customElements.define('my-react-element', MyReactElement)
44 |
--------------------------------------------------------------------------------
/examples/react-router/src/router.js:
--------------------------------------------------------------------------------
1 | import { initHashRouter } from '@bjornlu/svelte-router'
2 | import Home from './routes/Home.svelte'
3 | import User from './routes/User.svelte'
4 |
5 | // Path mode is not compatible with react-router. Hash mode is the only option
6 | initHashRouter([
7 | { path: '/', redirect: '/home' },
8 | { path: '/home/*', component: Home },
9 | { path: '/user/:id', component: User },
10 | { path: '/secret', redirect: '/' }
11 | ])
12 |
--------------------------------------------------------------------------------
/examples/react-router/src/routes/Home.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | Home
6 |
7 |
--------------------------------------------------------------------------------
/examples/react-router/src/routes/User.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | User
6 | Hello, your user ID is "{$route.params.id}"
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testMatch: ['**/__tests__/**/*.spec.ts'],
4 | transform: {
5 | // Svelte components are tested in Cypress
6 | '^.+.svelte$': 'jest-transform-stub'
7 | },
8 | globals: {
9 | 'ts-jest': {
10 | tsConfig: './__tests__/tsconfig.json'
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bjornlu/svelte-router",
3 | "description": "An easy-to-use SPA router for Svelte",
4 | "version": "0.5.0",
5 | "main": "dist/svelte-router.umd.js",
6 | "module": "dist/index.js",
7 | "svelte": "dist/index.js",
8 | "types": "dist/index.d.ts",
9 | "sideEffects": false,
10 | "files": [
11 | "dist"
12 | ],
13 | "author": {
14 | "name": "Bjorn Lu",
15 | "url": "https://bjornlu.com"
16 | },
17 | "license": "MIT",
18 | "homepage": "https://github.com/bluwy/svelte-router",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/bluwy/svelte-router.git"
22 | },
23 | "bugs": {
24 | "url": "https://github.com/bluwy/svelte-router/issues"
25 | },
26 | "keywords": [
27 | "svelte",
28 | "router"
29 | ],
30 | "scripts": {
31 | "clean": "rimraf dist",
32 | "build": "pnpm build:tsc && pnpm build:copy && pnpm build:bundle",
33 | "build:tsc": "tsc",
34 | "build:copy": "node ./scripts/copy-svelte.js",
35 | "build:bundle": "rollup -c",
36 | "test": "pnpm test:unit && pnpm test:e2e",
37 | "test:unit": "jest",
38 | "test:e2e": "server-test 'pnpm cy:setup' '10001|10002|10003' 'pnpm cy:run'",
39 | "cy:setup": "pnpm cy:setup:clean && pnpm cy:setup:build && pnpm cy:setup:serve",
40 | "cy:setup:clean": "rimraf cypress/test-app/public-*/build",
41 | "cy:setup:build": "rollup -c cypress/test-app/rollup.config.js",
42 | "cy:setup:serve": "node ./scripts/cy-serve.js",
43 | "cy:open": "cypress open",
44 | "cy:run": "cypress run --headless",
45 | "lint": "prettier --check '**/*.{html,js,ts,svelte}'",
46 | "format": "prettier --write '**/*.{html,js,ts,svelte}'",
47 | "prepublishOnly": "pnpm clean && pnpm build"
48 | },
49 | "husky": {
50 | "hooks": {
51 | "pre-commit": "lint-staged"
52 | }
53 | },
54 | "lint-staged": {
55 | "*.{html,js,ts,svelte}": "prettier --write"
56 | },
57 | "peerDependencies": {
58 | "svelte": "^3.20.0"
59 | },
60 | "dependencies": {
61 | "regexparam": "^1.3.0"
62 | },
63 | "devDependencies": {
64 | "@rollup/plugin-node-resolve": "^9.0.0",
65 | "@rollup/plugin-replace": "^2.4.2",
66 | "@rollup/plugin-typescript": "^5.0.2",
67 | "@types/jest": "^26.0.24",
68 | "cypress": "^5.6.0",
69 | "husky": "^4.3.8",
70 | "jest": "^26.6.3",
71 | "jest-transform-stub": "^2.0.0",
72 | "lint-staged": "^10.5.4",
73 | "prettier": "^2.4.1",
74 | "prettier-plugin-svelte": "^2.4.0",
75 | "rimraf": "^3.0.2",
76 | "rollup": "^2.58.3",
77 | "rollup-plugin-svelte": "^6.1.1",
78 | "sirv": "^1.0.18",
79 | "start-server-and-test": "^1.14.0",
80 | "svelte": "3.49.0",
81 | "svelte-preprocess": "^4.9.8",
82 | "ts-jest": "^26.5.6",
83 | "ts-node": "^9.1.1",
84 | "tslib": "^2.3.1",
85 | "typescript": "^4.4.4"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - examples/*
3 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve'
2 | import svelte from 'rollup-plugin-svelte'
3 | import pkg from './package.json'
4 |
5 | const name = pkg.name
6 | .replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3')
7 | .replace(/^\w/, (m) => m.toUpperCase())
8 | .replace(/-\w/g, (m) => m[1].toUpperCase())
9 |
10 | export default {
11 | input: pkg.module,
12 | output: { file: pkg.main, format: 'umd', name },
13 | plugins: [svelte(), resolve({ preventAssignment: true })]
14 | }
15 |
--------------------------------------------------------------------------------
/scripts/copy-svelte.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises
2 | const path = require('path')
3 | const svelte = require('svelte/compiler')
4 | const svelteConfig = require('../svelte.config')
5 |
6 | main()
7 |
8 | async function main() {
9 | const cwd = process.cwd()
10 |
11 | await fs.mkdir(path.resolve(cwd, 'dist/components'), { recursive: true })
12 |
13 | // Copy RouterView component
14 | await preprocessSvelte(
15 | path.resolve(cwd, 'src/components/RouterView.svelte'),
16 | path.resolve(cwd, 'dist/components/RouterView.svelte')
17 | )
18 |
19 | // Copy Link component
20 | await preprocessSvelte(
21 | path.resolve(cwd, 'src/components/Link.svelte'),
22 | path.resolve(cwd, 'dist/components/Link.svelte')
23 | )
24 | }
25 |
26 | async function preprocessSvelte(src, dest) {
27 | const srcCode = await fs.readFile(src, { encoding: 'utf-8' })
28 |
29 | let { code } = await svelte.preprocess(srcCode, svelteConfig.preprocess, {
30 | filename: src
31 | })
32 |
33 | // Remove lang to prevent preprocess on user side
34 | code = code.replace('lang="ts"', '')
35 |
36 | await fs.writeFile(dest, code, { encoding: 'utf-8' })
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/cy-serve.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 | const path = require('path')
3 | const sirv = require('sirv')
4 |
5 | main()
6 |
7 | async function main() {
8 | await serve()
9 | }
10 |
11 | async function serve() {
12 | const cwd = process.cwd()
13 |
14 | const servers = [
15 | { dir: 'public-hash', port: 10001, single: false },
16 | { dir: 'public-path', port: 10002, single: true },
17 | { dir: 'public-path-base', port: 10003, single: true }
18 | ]
19 |
20 | servers.forEach((server) => {
21 | http
22 | .createServer(
23 | sirv(path.resolve(cwd, `cypress/test-app/${server.dir}`), {
24 | dev: true,
25 | single: true
26 | })
27 | )
28 | .listen(server.port, () => {
29 | console.log(
30 | green(server.dir) + ' -> ' + green(`http://localhost:${server.port}`)
31 | )
32 | })
33 | })
34 | }
35 |
36 | function green(text) {
37 | return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m'
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Link.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/RouterView.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
35 |
36 | {#if canRender}
37 | {#if component != null}
38 |
39 | {#if hasChildren}
40 |
41 | {/if}
42 |
43 | {:else if hasChildren}
44 |
45 | {/if}
46 | {/if}
47 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | import { writable, Readable } from 'svelte/store'
2 | import { Router, Route } from './router/base'
3 | import { HashRouter } from './router/hash-router'
4 | import { PathRouter } from './router/path-router'
5 | import { RouteRecord } from './types'
6 |
7 | let globalRouter: Router | undefined
8 |
9 | const writableRoute = writable({
10 | path: '',
11 | params: {},
12 | matched: [],
13 | search: new URLSearchParams(),
14 | hash: ''
15 | })
16 |
17 | export const route: Readable = { subscribe: writableRoute.subscribe }
18 |
19 | export const navigate: Router['navigate'] = function () {
20 | if (globalRouter != null) {
21 | // @ts-expect-error
22 | return globalRouter.navigate(...arguments)
23 | } else {
24 | throw new Error('Router must be initialized before calling navigate')
25 | }
26 | }
27 |
28 | export const createLink: Router['createLink'] = function () {
29 | if (globalRouter != null) {
30 | // @ts-expect-error
31 | return globalRouter.createLink(...arguments)
32 | } else {
33 | throw new Error('Router must be initialized before calling createLink')
34 | }
35 | }
36 |
37 | export function initHashRouter(routes: RouteRecord[]) {
38 | initRouter(new HashRouter(routes))
39 | }
40 |
41 | export function initPathRouter(routes: RouteRecord[]) {
42 | initRouter(new PathRouter(routes))
43 | }
44 |
45 | function initRouter(router: Router) {
46 | if (globalRouter == null) {
47 | globalRouter = router
48 | globalRouter.currentRoute.subscribe((v) => writableRoute.set(v))
49 | } else {
50 | throw new Error('Router already initialized. Cannot re-initialize router.')
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import './location-change-shim'
2 | export { default as RouterView } from './components/RouterView.svelte'
3 | export { default as Link } from './components/Link.svelte'
4 | export { LinkState, Route } from './router/base'
5 | export { LocationInput, RouteRecord } from './types'
6 | export {
7 | route,
8 | createLink,
9 | navigate,
10 | initHashRouter,
11 | initPathRouter
12 | } from './global'
13 |
--------------------------------------------------------------------------------
/src/location-change-shim.ts:
--------------------------------------------------------------------------------
1 | // Listen to location changes that are triggered by:
2 | // - pushState
3 | // - replaceState
4 | // - onpopstate
5 | // - onhashchange
6 |
7 | export const LOCATION_CHANGE = 'svelterouterlocationchange'
8 |
9 | const originalPushState = history.pushState
10 | const originalReplaceState = history.replaceState
11 |
12 | let prevLocation = ''
13 |
14 | function dispatch() {
15 | if (window.location.href !== prevLocation) {
16 | window.dispatchEvent(new CustomEvent(LOCATION_CHANGE))
17 | prevLocation = window.location.href
18 | }
19 | }
20 |
21 | history.pushState = function (...args) {
22 | originalPushState.apply(this, args)
23 | dispatch()
24 | }
25 |
26 | history.replaceState = function (...args) {
27 | originalReplaceState.apply(this, args)
28 | dispatch()
29 | }
30 |
31 | window.addEventListener('popstate', dispatch)
32 | window.addEventListener('hashchange', dispatch)
33 |
--------------------------------------------------------------------------------
/src/matcher.ts:
--------------------------------------------------------------------------------
1 | import regexparam from 'regexparam'
2 | import { ensureTrailingSlash, formatPath, joinPaths } from './util'
3 | import { RouteRecord } from './types'
4 |
5 | export interface MatchedRoute {
6 | /** The current path, e.g. "/foo/bar" */
7 | path: string
8 | /**
9 | * Key-value pairs of route dynamic segments and wildcards. Access wildcard
10 | * values with the 'wild' key.
11 | */
12 | params: Record
13 | /** Array of all matched route records for this path */
14 | matched: RouteRecord[]
15 | }
16 |
17 | interface RouteMatchData {
18 | /** The current formatted path */
19 | path: string
20 | /** The pattern generated by regexparam */
21 | pattern: RegExp
22 | /** The keys the pattern will match to */
23 | keys: string[]
24 | /** Array of all matched routes for this path */
25 | matched: RouteRecord[]
26 | }
27 |
28 | /** Metadata used on route parent when traversing routes in buildDatas */
29 | type RouteMatchParent = Pick
30 |
31 | export class RouteMatcher {
32 | private readonly matchDatas: RouteMatchData[] = []
33 |
34 | constructor(routes: RouteRecord[] = []) {
35 | this.buildDatas(routes)
36 | }
37 |
38 | /** Finds a route based on target path and the computed matchers. */
39 | matchRoute(path: string): MatchedRoute | undefined {
40 | // Add trailing slash to route path so it properly matches nested routes too.
41 | // e.g. /foo should match /foo/*
42 | const matchPath = ensureTrailingSlash(formatPath(path))
43 |
44 | for (const matchData of this.matchDatas) {
45 | const params: Record = {}
46 | const matchResult = matchPath.match(matchData.pattern)
47 |
48 | if (matchResult) {
49 | for (let i = 0; i < matchData.keys.length; i++) {
50 | params[matchData.keys[i]] = matchResult[i + 1]
51 | }
52 |
53 | if ('wild' in params) {
54 | params.wild = formatPath(params.wild)
55 | }
56 |
57 | return {
58 | path,
59 | params,
60 | matched: matchData.matched
61 | }
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * Convert the routes as datas that contains information for path matching.
68 | * This will recursively traverse child routes and flatten into `matchDatas`.
69 | */
70 | private buildDatas(
71 | routes: RouteRecord[],
72 | parentData: RouteMatchParent = { path: '', matched: [] }
73 | ) {
74 | routes.forEach((route) => {
75 | // Cumulative metadata when traversing parents
76 | const parent: RouteMatchParent = {
77 | path: joinPaths(parentData.path, route.path),
78 | matched: parentData.matched.concat(route)
79 | }
80 |
81 | if (route.children?.length) {
82 | this.buildDatas(route.children, parent)
83 | } else {
84 | this.matchDatas.push({
85 | ...parent,
86 | ...regexparam(parent.path)
87 | })
88 | }
89 | })
90 |
91 | // Make sure "/" doesn't fall through if the routes' paths do.
92 | // Example route config:
93 | // "/foo"
94 | // - "/bar"
95 | // match('/foo') should render "/foo" component but with empty child
96 | if (!routes.some((route) => ['', '/', '*', '/*'].includes(route.path))) {
97 | this.matchDatas.push({
98 | ...parentData,
99 | ...regexparam(parentData.path)
100 | })
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/router/base.ts:
--------------------------------------------------------------------------------
1 | import { Readable, readable, derived } from 'svelte/store'
2 | import { MatchedRoute, RouteMatcher } from '../matcher'
3 | import { LocationInput, RouteRecord } from '../types'
4 | import { LOCATION_CHANGE } from '../location-change-shim'
5 | import { parseLocationInput, formatPath } from '../util'
6 |
7 | export interface Route extends MatchedRoute {
8 | search: URLSearchParams
9 | hash: string
10 | }
11 |
12 | export interface LinkState {
13 | href: string
14 | isActive: boolean
15 | isExactActive: boolean
16 | }
17 |
18 | export abstract class Router {
19 | readonly currentRoute: Readable
20 | private readonly routeMatcher: RouteMatcher
21 |
22 | constructor(routes: RouteRecord[]) {
23 | this.routeMatcher = new RouteMatcher(routes)
24 |
25 | this.currentRoute = readable(this.getCurrentRoute(), (set) => {
26 | const handleChange = () => set(this.getCurrentRoute())
27 |
28 | window.addEventListener(LOCATION_CHANGE, handleChange)
29 |
30 | return () => window.removeEventListener(LOCATION_CHANGE, handleChange)
31 | })
32 |
33 | // Format URL on page load
34 | this.navigate(this.getCurrentLocationInput(), true)
35 | }
36 |
37 | /**
38 | * Navigates to the url but do not create a new entry in the history
39 | */
40 | protected abstract replace(url: string): void
41 | /**
42 | * Navigates to the url, leaving behind a trace in the history
43 | */
44 | protected abstract push(url: string): void
45 | protected abstract getCurrentPath(): string
46 | protected abstract getCurrentLocationInput(): LocationInput
47 | protected abstract getPath(to: LocationInput): string | undefined
48 | protected abstract createUrl(to: LocationInput): string
49 | protected abstract createLinkHref(to: LocationInput): string
50 | protected abstract replaceParams(
51 | to: LocationInput,
52 | params?: Record
53 | ): LocationInput
54 |
55 | /**
56 | * Navigate using an offset in the current history. Works the same way as
57 | * [`history.go`](https://developer.mozilla.org/en-US/docs/Web/API/History/go).
58 | */
59 | navigate(to: number): void
60 | /**
61 | * Navigate to a route using a path string or a location descriptor object
62 | */
63 | navigate(to: string | LocationInput, replace?: boolean): void
64 | navigate(to: number | string | LocationInput, replace = false) {
65 | if (typeof to === 'number') {
66 | history.go(to)
67 | return
68 | }
69 |
70 | if (typeof to === 'string') {
71 | to = parseLocationInput(to)
72 | }
73 |
74 | const url = this.createUrl(this.replaceParams(to))
75 |
76 | if (replace) {
77 | this.replace(url)
78 | } else {
79 | this.push(url)
80 | }
81 | }
82 |
83 | /** Returns a readable store that contains the given link's information */
84 | createLink(to: string | LocationInput): Readable {
85 | const input = typeof to === 'string' ? parseLocationInput(to) : to
86 |
87 | return derived(this.currentRoute, ($currentRoute) => {
88 | const replacedInput = this.replaceParams(input, $currentRoute.params)
89 | const href = this.createLinkHref(replacedInput)
90 | const path = this.getPath(replacedInput)
91 |
92 | if (path == null) {
93 | return {
94 | href,
95 | isActive: false,
96 | isExactActive: false
97 | }
98 | }
99 |
100 | const formattedPath = formatPath(path)
101 | const routePath = $currentRoute.path
102 |
103 | return {
104 | href,
105 | isActive: routePath.startsWith(formattedPath),
106 | isExactActive: routePath === formattedPath
107 | }
108 | })
109 | }
110 |
111 | private getCurrentRoute(): Route {
112 | const currentPath = this.getCurrentPath()
113 | const matchedRoute = this.routeMatcher.matchRoute(currentPath)
114 |
115 | return {
116 | path: currentPath,
117 | search: new URLSearchParams(window.location.search),
118 | hash: window.location.hash,
119 | params: matchedRoute?.params ?? {},
120 | matched: matchedRoute?.matched ?? []
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/router/hash-router.ts:
--------------------------------------------------------------------------------
1 | import { get } from 'svelte/store'
2 | import { Router } from './base'
3 | import { LocationInput } from '../types'
4 | import { formatPath, replacePathParams } from '../util'
5 |
6 | export class HashRouter extends Router {
7 | getCurrentPath() {
8 | return formatPath(window.location.hash.slice(1))
9 | }
10 |
11 | getCurrentLocationInput(): LocationInput {
12 | return {
13 | hash: '#' + this.getCurrentPath(),
14 | search: window.location.search
15 | }
16 | }
17 |
18 | getPath(to: LocationInput) {
19 | return to.path ?? to.hash?.slice(1)
20 | }
21 |
22 | createUrl(to: LocationInput) {
23 | const url = new URL(window.location.href)
24 |
25 | if (to.path) {
26 | url.hash = '#' + to.path
27 | } else {
28 | url.hash = to.hash ?? ''
29 | }
30 |
31 | url.search = to.search
32 | ? '?' + new URLSearchParams(to.search).toString()
33 | : ''
34 |
35 | return url.toString()
36 | }
37 |
38 | createLinkHref(to: LocationInput) {
39 | const search = to.search ?? ''
40 | const hash = to.path != null ? '#' + to.path : to.hash ?? ''
41 |
42 | return search + hash
43 | }
44 |
45 | replaceParams(to: LocationInput, params?: Record) {
46 | const newTo = { ...to }
47 | const routeParams = params ?? get(this.currentRoute).params
48 |
49 | if (newTo.path) {
50 | newTo.path = replacePathParams(newTo.path, routeParams)
51 | }
52 |
53 | if (newTo.hash) {
54 | newTo.hash = replacePathParams(newTo.hash, routeParams)
55 | }
56 |
57 | return newTo
58 | }
59 |
60 | replace(url: string) {
61 | location.replace(url)
62 | }
63 |
64 | push(url: string) {
65 | location.assign(url)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/router/path-router.ts:
--------------------------------------------------------------------------------
1 | import { get } from 'svelte/store'
2 | import { Router } from './base'
3 | import { LocationInput } from '../types'
4 | import { formatPath, getBasePath, joinPaths, replacePathParams } from '../util'
5 |
6 | const basePath = /*#__PURE__*/ getBasePath()
7 |
8 | export class PathRouter extends Router {
9 | getCurrentPath() {
10 | return formatPath(window.location.pathname.replace(basePath, ''))
11 | }
12 |
13 | getCurrentLocationInput(): LocationInput {
14 | return {
15 | path: this.getCurrentPath(),
16 | hash: window.location.hash,
17 | search: window.location.search
18 | }
19 | }
20 |
21 | getPath(to: LocationInput) {
22 | return to.path
23 | }
24 |
25 | createUrl(to: LocationInput) {
26 | const url = new URL(window.location.href)
27 |
28 | if (to.path) {
29 | url.pathname = joinPaths(basePath, to.path)
30 | }
31 |
32 | url.search = to.search
33 | ? '?' + new URLSearchParams(to.search).toString()
34 | : ''
35 |
36 | url.hash = to.hash ?? ''
37 |
38 | return url.toString()
39 | }
40 |
41 | createLinkHref(to: LocationInput) {
42 | const path = to.path ?? ''
43 | const search = to.search ?? ''
44 | const hash = to.hash ?? ''
45 |
46 | return joinPaths(basePath, path) + search + hash
47 | }
48 |
49 | replaceParams(to: LocationInput, params?: Record) {
50 | const newTo = { ...to }
51 | const routeParams = params ?? get(this.currentRoute).params
52 |
53 | if (newTo.path) {
54 | newTo.path = replacePathParams(newTo.path, routeParams)
55 | }
56 |
57 | return newTo
58 | }
59 |
60 | replace(url: string) {
61 | history.replaceState(url, '', url)
62 | }
63 |
64 | push(url: string) {
65 | history.pushState(url, '', url)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export type Thunk = T | (() => T)
2 |
3 | export type Promisable = T | Promise
4 |
5 | export interface LocationInput {
6 | path?: string
7 | search?: ConstructorParameters[0]
8 | hash?: string
9 | }
10 |
11 | export interface RouteRecord {
12 | /** The route path, e.g. "/foo" */
13 | path: string
14 | /** Svelte component */
15 | component?: Thunk>
16 | /** Redirect to another path if route match */
17 | redirect?: Thunk>
18 | /**
19 | * Array of children routes. If defined, this route component requires a
20 | * ` ` component to render the children routes.
21 | */
22 | children?: RouteRecord[]
23 | }
24 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import { Thunk, Promisable, LocationInput } from './types'
2 |
3 | export function getBasePath() {
4 | return document.getElementsByTagName('base').length > 0
5 | ? document.baseURI.replace(window.location.origin, '')
6 | : '/'
7 | }
8 |
9 | export function parseLocationInput(to: string): LocationInput {
10 | const url = new URL(to, 'https://example.com')
11 |
12 | return {
13 | path: to.startsWith('/') ? url.pathname : undefined,
14 | search: url.search,
15 | hash: url.hash
16 | }
17 | }
18 |
19 | /** Replace dynamic segments in path, e.g. `/user/:id` => `/user/foo` */
20 | export function replacePathParams(
21 | path: string,
22 | params: Record
23 | ) {
24 | return path.replace(/:([^/]+)/g, (o, v) => params[v] ?? o)
25 | }
26 |
27 | /** Makes sure path has leading "/" and no trailing "/" */
28 | export function formatPath(path: string) {
29 | if (path.endsWith('/')) {
30 | path = path.slice(0, -1)
31 | }
32 |
33 | if (!path.startsWith('/')) {
34 | path = '/' + path
35 | }
36 |
37 | return path
38 | }
39 |
40 | /** Joins multiple path and also formats it */
41 | export function joinPaths(...paths: string[]) {
42 | return paths
43 | .map(formatPath)
44 | .filter((v) => v !== '/')
45 | .join('')
46 | }
47 |
48 | export function ensureTrailingSlash(path: string) {
49 | if (!path.endsWith('/')) {
50 | path += '/'
51 | }
52 |
53 | return path
54 | }
55 |
56 | export function handleThunk(thunk: Thunk): T {
57 | return typeof thunk === 'function' ? (thunk as any)() : thunk
58 | }
59 |
60 | export function handleComponentThunk(thunk: Thunk): T {
61 | // Svelte components are classes/functions so thunk won't actually work.
62 | // The workaround is by assuming this syntax `() => import` to be used.
63 | // Since lambda has no prototype, so we check it before calling thunk.
64 | return typeof thunk === 'function' && !('prototype' in thunk)
65 | ? (thunk as any)()
66 | : thunk
67 | }
68 |
69 | export function handlePromisable(
70 | promisable: Promisable,
71 | callback: (value: T) => void
72 | ) {
73 | if (promisable instanceof Promise) {
74 | promisable.then(callback)
75 | } else {
76 | callback(promisable)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | const sveltePreprocess = require('svelte-preprocess')
2 |
3 | module.exports = {
4 | preprocess: sveltePreprocess()
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "moduleResolution": "node",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "types": ["svelte"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "declaration": true
7 | },
8 | "include": ["./src/**/*"],
9 | "exclude": ["node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------