The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitattributes
├── .github
    ├── FUNDING.yml
    └── workflows
    │   ├── ci-tests.yml
    │   └── size.yml
├── .gitignore
├── LICENSE
├── README.md
├── assets
    ├── icon_duotone.svg
    ├── logo.svg
    ├── wouter-skate.svg
    └── wouter.svg
├── bun.lock
├── package.json
├── packages
    ├── wouter-preact
    │   ├── package.json
    │   ├── rollup.config.js
    │   ├── src
    │   │   └── preact-deps.js
    │   ├── test
    │   │   ├── preact-ssr.test.tsx
    │   │   ├── preact.test-d.ts
    │   │   └── preact.test.tsx
    │   ├── tsconfig.json
    │   ├── types
    │   │   ├── index.d.ts
    │   │   ├── location-hook.d.ts
    │   │   ├── memory-location.d.ts
    │   │   ├── router.d.ts
    │   │   ├── use-browser-location.d.ts
    │   │   └── use-hash-location.d.ts
    │   └── vitest.config.ts
    └── wouter
    │   ├── package.json
    │   ├── rollup.config.js
    │   ├── setup-vitest.ts
    │   ├── src
    │       ├── index.js
    │       ├── memory-location.js
    │       ├── paths.js
    │       ├── react-deps.js
    │       ├── use-browser-location.js
    │       ├── use-hash-location.js
    │       ├── use-sync-external-store.js
    │       └── use-sync-external-store.native.js
    │   ├── test
    │       ├── global-this-at-component-level.test.tsx
    │       ├── global-this-at-top-level.test.ts
    │       ├── history-patch.test.ts
    │       ├── link.test-d.tsx
    │       ├── link.test.tsx
    │       ├── location-hook.test-d.ts
    │       ├── match-route.test-d.ts
    │       ├── memory-location.test-d.ts
    │       ├── memory-location.test.ts
    │       ├── nested-route.test.tsx
    │       ├── parser.test.tsx
    │       ├── redirect.test-d.tsx
    │       ├── redirect.test.tsx
    │       ├── route.test-d.tsx
    │       ├── route.test.tsx
    │       ├── router.test-d.tsx
    │       ├── router.test.tsx
    │       ├── ssr.test.tsx
    │       ├── switch.test.tsx
    │       ├── test-utils.ts
    │       ├── use-browser-location.test-d.ts
    │       ├── use-browser-location.test.tsx
    │       ├── use-hash-location.test-d.ts
    │       ├── use-hash-location.test.tsx
    │       ├── use-location.test.tsx
    │       ├── use-params.test-d.ts
    │       ├── use-params.test.tsx
    │       ├── use-route.test-d.ts
    │       ├── use-route.test.tsx
    │       ├── use-search-params.test.tsx
    │       └── use-search.test.tsx
    │   ├── tsconfig.json
    │   ├── types
    │       ├── index.d.ts
    │       ├── location-hook.d.ts
    │       ├── memory-location.d.ts
    │       ├── router.d.ts
    │       ├── use-browser-location.d.ts
    │       └── use-hash-location.d.ts
    │   └── vitest.config.ts
└── vitest.workspace.ts


/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | 


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 | 
3 | github: ["molefrog"]
4 | 


--------------------------------------------------------------------------------
/.github/workflows/ci-tests.yml:
--------------------------------------------------------------------------------
 1 | name: Run Tests & Linters
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: "*"
 6 |   pull_request:
 7 |     branches: "*"
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 | 
13 |     env:
14 |       FORCE_COLOR: true
15 | 
16 |     steps:
17 |       - uses: actions/checkout@v4
18 | 
19 |       - name: Setup Bun
20 |         uses: oven-sh/setup-bun@v2
21 |         with:
22 |           bun-version: latest
23 | 
24 |       - name: Install Dependencies
25 |         run: bun install --frozen-lockfile
26 | 
27 |       - name: Build packages
28 |         run: npm run build
29 | 
30 |       - name: Run test
31 |         run: bun run test -- --run --coverage
32 | 
33 |       - name: Run type check
34 |         run: bun run lint-types
35 | 
36 |       - name: Lint Sources with ESLint
37 |         run: bun run lint
38 | 
39 |       - name: Upload Coverage Report to Codecov
40 |         run: bash <(curl -s https://codecov.io/bash) -t 7a260fc2-03ff-4e98-b0a5-11a2d5c53a29
41 | 


--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
 1 | name: Size
 2 | on: [pull_request]
 3 | jobs:
 4 |   size:
 5 |     runs-on: ubuntu-latest
 6 |     env:
 7 |       CI_JOB_NUMBER: 1
 8 |     steps:
 9 |       - uses: actions/checkout@v3
10 |       - uses: andresz1/size-limit-action@v1
11 |         with:
12 |           github_token: ${{ secrets.GITHUB_TOKEN }}
13 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # NPM
 2 | npm-debug.log*
 3 | node_modules/
 4 | .npm
 5 | 
 6 | # IDEs
 7 | .vscode/
 8 | *.code-workspace
 9 | 
10 | # bundler
11 | esm/
12 | .cache
13 | 
14 | # OSX
15 | .DS_Store
16 | .AppleDouble
17 | .LSOverride
18 | 
19 | # test coverage
20 | coverage/
21 | 
22 | # vitest internals
23 | tsconfig.vitest-temp.json
24 | 
25 | # type definitions in the project root are copied from types/ folder
26 | # before publishing, so they should be ignored
27 | /*.d.ts
28 | 
29 | # README is copied from the root folder
30 | packages/wouter/README.md
31 | packages/wouter-preact/README.md
32 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | This is free and unencumbered software released into the public domain.
 2 | 
 3 | Anyone is free to copy, modify, publish, use, compile, sell, or
 4 | distribute this software, either in source code form or as a compiled
 5 | binary, for any purpose, commercial or non-commercial, and by any
 6 | means.
 7 | 
 8 | In jurisdictions that recognize copyright laws, the author or authors
 9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 | 
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 | 
24 | For more information, please refer to <https://unlicense.org>
25 | 


--------------------------------------------------------------------------------
/assets/icon_duotone.svg:
--------------------------------------------------------------------------------
1 | <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="MAIN" x="0" y="0" style="enable-background:new 0 0 256 256" version="1.1" viewBox="0 0 256 256"><style>.st2{opacity:.5;fill:#010101}</style><path id="THING" d="M236.7 152.3c-2.4-2.7-7.6-4.1-10.6-1.7-6.6 5.4 1.4 10.4 6.8 9.5.9-.1 1.7-.5 2.5-1 3.3-2.3 3.1-4.9 1.3-6.8z" style="fill:#010101;stroke:#010101;stroke-width:4;stroke-linecap:round;stroke-linejoin:round"/><path id="HAND" d="M237.9 208.9c-2.1-2.9-3.5-6-4.2-8.7-1.3-4.8-1.4-9.9-.3-14.7 1.2-4.7 5-9.8 9.6-12.8-4.3-5-11.8-6.7-18.1-4.6-7 2.3-12 8.5-14.2 15.3-1.4 4.2-2.1 7.4-3.3 10.1s-3 4.9-6.7 7.4c-5.5 3.6-12 5.2-18.5 4.8-12.9-.7-25.7-7.1-33-17.5-8.3-11.8-9.9-27.8-5.4-41.5 3.8-11.6 12.2-22.9 22.6-29.5 9.4-6 22.9-11.5 33.9-8.9 3.8.9 6 3.3 7.6 6.5 1.6 3.2 2.5 7.1 3.6 10.7 1.3 4.2 3.8 8.6 7.4 11.7 3.5 3.2 8 5.2 13.1 4.7 3.1-.3 6-1.7 8.4-3.7-3.8-2.1-6.3-5.4-7.8-9.2-1.7-4.4-2-9.4-1.5-14.4 1.1-9.5 5.3-19.1 10.8-24.9-2.9-5.5-6.8-10.5-11.7-14.6-12.6-10.6-30.5-13.4-46.4-9.6-15.9 3.8-29.8 13.7-40.4 26-9.2 10.6-15.7 23.1-21.4 35.8 2.9-17 12-32.8 23.6-45.9C158.7 66.6 175 55 191.2 43.6c4.5-3.2 9.3-6.5 12.1-11.6 2.9-5.4 3.1-11.8.5-17.3-1.7-3.4-4.3-6.2-7.5-8.1-4.2 5-9.5 8.9-15.5 11.5-5.6 2.2-11.5 4-17.9 4.1-3.6.1-10.2.2-15.2-2.6-1.9 1.5-3.8 3.1-5.6 4.7-15.6 14.2-27.6 32-36.1 51.3-5.2 11.9-11.4 25.8-14.8 39.8.8-12.3 2.5-24.7 5.7-36.1 4.9-17.9 12-35.3 15.3-53.9.9-5.1 1.4-11-1.9-15.5-2.3-3.1-5.9-4.6-9.7-4.8h-1.4c-1.6 14-10.5 25.3-21.6 30.1-1.6.7-3.7 1-5.9 1-1.4 0-2.8 0-4.1-.4-10.6 23.6-14.6 49-16 74.8l-2-42.6c-.8-16.9-1.8-34.7-10.3-49.6-2-3.6-5-7-8.5-9.3-3.5-2.3-7.6-3.3-11.7-1.8-1 .4-1.9.8-2.7 1.4 3.5 6 4.5 14 3.4 21.5-1.1 7.6-4.1 14.9-9.3 19.4.7 7.1 1.6 14.1 2.4 21.1 2.9 27.1 2.6 54.7 2.6 82 0 22.5-3 45-1.4 67.7.3 4 .3 9.2 1.2 14.1s2.4 9.7 6 12.8c6.5 5.6 17.8 5 25 5.2 9.8.3 19.7-.1 29.6.2 24.4.6 48.7.3 73-.2h.9c22-.3 43.9.6 66-1 8.1-.6 17.3-1.8 22.9-8.3 3.8-4.5 4.9-10.5 5.7-16.1.6-4 1-8 1.4-12.1-3.3-1-6-3.4-7.9-6.1z" style="fill:#010101"/><g id="NAILS"><path id="_x35_" d="M245.2 176.1c-.2-.5-.5-1.1-.8-1.6-.4-.7-.8-1.3-1.3-1.8-4.6 3-8.4 8.1-9.6 12.8-1.2 4.9-1.1 9.9.3 14.7.7 2.6 2.1 5.8 4.2 8.7 2 2.7 4.6 5.1 8 6.2.1-1.4.3-2.8.4-4.2.6-7.6.9-15.3.8-22.9-.2-3.9-.3-8.1-2-11.9z" class="st2"/><path id="_x34_" d="M243.6 93.8c-.6-1.3-1.2-2.6-1.9-3.9-5.5 5.8-9.7 15.4-10.8 24.9-.6 5-.2 10 1.5 14.4 1.5 3.8 4.1 7 7.8 9.2 1.1-.9 2-1.9 2.9-3 .7-1 1.4-2 1.9-3 2.5-5 3.1-10.6 3.1-16.1 0-7.8-1.5-15.5-4.5-22.5z" class="st2"/><path id="_x33_" d="M192.3 4.9c-.8-.3-1.7-.5-2.5-.7-1.5-.2-3.1-.3-4.7-.3s-3.1.2-4.6.5C169.8 6 160 10.7 151.2 16.8c-1.2.9-2.5 1.8-3.7 2.7 4.9 2.8 11.6 2.7 15.2 2.6 6.4-.1 12.2-1.9 17.9-4.1 6-2.5 11.3-6.5 15.5-11.5-1.2-.6-2.5-1.2-3.8-1.6z" class="st2"/><path id="_x32_" d="M95.1 5.7c-1.9.4-3.8 1.1-5.5 2-8.9 4.8-14.7 13.6-19.2 22.4-.3.6-.6 1.3-.9 1.9-.6 1.3-1.2 2.6-1.8 3.8 1.3.4 2.7.4 4.1.4 2.2 0 4.3-.3 5.9-1 11.1-4.8 19.9-16.1 21.6-30.1-1.5.1-2.9.3-4.2.6z" class="st2"/><path id="_x31_" d="M16.4 8.7c-1.2.8-2.1 1.8-2.9 3-.8 1.2-1.5 2.4-2 3.8-1.4 3.6-1.9 7.8-2 11.6-.2 5.7.1 11.5.5 17.2.1 1.8.3 3.5.5 5.3 5.2-4.5 8.3-11.8 9.3-19.4 1.1-7.5.2-15.5-3.4-21.5z" class="st2"/></g></svg>


--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 | <svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><style>.cls-1,.cls-2{fill:#010101;stroke:#010101;stroke-linecap:round;stroke-linejoin:round;stroke-width:4px}.cls-2{fill:none;stroke:#fff}</style></defs><path class="cls-1" d="M149.79 251.54h-.93c-24.32.47-48.64.73-73 .15-9.9-.24-19.8.11-29.6-.18-7.27-.21-18.43.15-24.42-5-6.55-5.68-6.19-18.08-6.78-26.21-1.63-22.54 1.37-45.06 1.4-67.6 0-27.27.33-54.87-2.59-82.06-1.56-14.51-3.76-29-3.44-43.6.17-7.39 2-16.23 9-18.81 7.44-2.77 15.08 3.83 19 10.71 8.41 14.66 9.42 32.26 10.22 49.14l3.35 70.4c.06-37.26 2.52-74.59 19.23-107.89C75.66 21.84 81.36 13.21 90 8.6c6.25-3.34 15.16-3.72 19.38 2 3 4.08 2.56 9.72 1.68 14.73-3.24 18.49-10.36 35.78-15.28 53.8-4.88 17.81-6.78 38.19-6.1 56.67-.78-19.74 9.36-42 17.1-59.75 8.38-19.2 20.28-36.86 35.81-51 10.71-9.74 23.72-17.45 38-19.68a27.57 27.57 0 019-.16 18.29 18.29 0 0113.09 10 18.28 18.28 0 01-.47 16.44c-2.64 4.8-7.29 8.11-11.78 11.27-16.25 11.42-32.55 23.12-45.77 38S121.38 114.08 120 133.9c6.41-14.74 13.5-29.46 24-41.59s24.26-21.92 39.88-25.68 33.19-.91 45.52 9.4c11.91 10 17.62 24.89 17.73 40.23 0 5.36-.58 10.86-3 15.62s-7 8.67-12.35 9.22c-9.53 1-16.89-7.56-19.4-15.71-2.15-7-3.6-15.91-11.92-17.9-11.43-2.72-25.09 2.91-34.65 9-10.57 6.7-19.09 18.19-23 30-4.66 14-3 30.31 5.49 42.4 7.53 10.7 20.63 17.2 33.8 17.9a31.59 31.59 0 0019.13-5c7.79-5.15 7.66-9.73 10.41-18.07 2.17-6.56 7-12.48 13.6-14.67s14.68 0 18.25 6c2.31 3.85 2.52 8.55 2.56 13a260.84 260.84 0 01-2.63 39c-.79 5.53-1.89 11.32-5.51 15.57-5.23 6.15-14.12 7.37-22.17 7.93-22.04 1.54-43.92.64-65.95.99z"/><path class="cls-1" d="M236.7 152.26c-2.42-2.66-7.64-4.09-10.56-1.72-6.59 5.36 1.37 10.44 6.84 9.51a6.12 6.12 0 002.48-1c3.27-2.23 3.01-4.84 1.24-6.79z"/><path class="cls-2" d="M7.73 49.26c11.07-7.71 14-30.06 5.93-41.14M66.87 33.24c1.81 1.53 7.75 1.15 9.92.2 10.72-4.66 19.35-15.82 20.52-29.67M148.16 17.62c4.16 2.58 10.88 2.63 14.5 2.57 6-.1 11.64-1.8 17.18-3.94a38.87 38.87 0 0016-12.48M244.1 90.41c-10.93 10.21-18.38 39.74-1.19 47.07M245.68 173.48c-4.75 2.38-9.2 8-10.36 12.48a27.23 27.23 0 00.25 13.74c1.27 4.77 5.27 12 11.56 13.74"/></svg>


--------------------------------------------------------------------------------
/assets/wouter-skate.svg:
--------------------------------------------------------------------------------
1 | <svg width="430" height="430" xmlns="http://www.w3.org/2000/svg"><g fill="#1D1D1B" fill-rule="nonzero"><path d="M391.68 104.27c3.11 8.42 6.43 17.64 4.73 26.74-1.2 6.41-4.59 14.55-9.79 18.68-9.7 7.71-28.29 5.56-38.54.2-2.14-1.12-4 2.12-1.89 3.24 11.59 6.07 31.76 8.25 42.71-.43 5.54-4.39 9-12.24 10.73-19 2.63-10.36-.76-20.78-4.34-30.46-.83-2.24-4.45-1.27-3.62 1l.01.03z"/><path d="M414.07 96.93c9.32 18.09 15.58 42.95 8.21 62.73-7.47 20-25.89 33.22-46.51 36.93-2.37.43-1.37 4 1 3.62 21.58-3.87 40.36-17.71 48.64-38.29 8.43-21 1.85-47.56-8.1-66.87-1.11-2.15-4.34-.25-3.24 1.89v-.01zM363.46 198.92a133.28 133.28 0 0 1-53.46 1.64c-2.36-.42-3.37 3.19-1 3.62a137.86 137.86 0 0 0 55.44-1.64c2.35-.56 1.35-4.18-1-3.62h.02z"/><path d="M393.94 103.89a89.68 89.68 0 0 1 10.81-4c2.37-.73 7.17-3.11 9.62-2.83l-1.81-2.37v.13l2.31-1.31h-.1c-2.34-.61-3.34 3-1 3.62h.1a1.92 1.92 0 0 0 2.31-1.31v-.13a1.9 1.9 0 0 0-1.81-2.37c-3-.35-7.78 2.08-10.61 3a97.3 97.3 0 0 0-11.76 4.33c-2.19 1-.29 4.24 1.89 3.24h.05zM399.1 119.15c7.1-1.91 14-5 21-7.52 2.25-.82 1.28-4.45-1-3.62-6.9 2.53-13.85 5.61-21 7.52-2.33.63-1.34 4.24 1 3.62z"/><path d="M399.42 101.77a216.54 216.54 0 0 1 4.09 11.57c.74 2.29 4.36 1.31 3.62-1a216.54 216.54 0 0 0-4.09-11.57c-.86-2.23-4.49-1.26-3.62 1zM406.45 98.89a111.52 111.52 0 0 1 4.44 11.66c.73 2.29 4.35 1.31 3.62-1A120.18 120.18 0 0 0 409.69 97c-1-2.2-4.22-.29-3.24 1.89zM393.94 99.66c-4.52-3.1-9.19-6.14-13.48-9.56a8 8 0 0 1-1.7-1.53c-1.45-2.14.34-3.27 2.38-2.85 2.47.51 4.91 2.14 7 3.5C390.23 90.58 392 87.3 390 86c-3.11-2-6.86-4.3-10.72-4.17-3.07.1-5.29 2.06-5.09 5.31.2 3.25 3 5.23 5.39 7 4.09 3 8.26 5.93 12.45 8.8 2 1.37 3.87-1.88 1.89-3.24l.02-.04z"/><path d="M391.41 86.75l-3.22-7.1c-.67-1.47-4.3-7.78-3.09-9.45 1.21-1.67 3.71.39 4.46 1.13a14.23 14.23 0 0 1 2.27 3.21 81.32 81.32 0 0 1 4 8.74c.91 2.2 4.54 1.23 3.62-1-1.77-4.28-3.44-9-6.4-12.65-1.9-2.33-5.84-5.28-9.11-4-7.51 3 2.36 18.86 4.23 23 1 2.19 4.23.29 3.24-1.89v.01z"/><path d="M391.42 68.06c-.64-1 .47-2.15 1.25-2.64 1.39-.89 2.43-.42 3.4.73a32.08 32.08 0 0 1 2.78 4.28c1.67 2.84 3.81 5.87 5.08 8.89.92 2.19 4.55 1.23 3.62-1-1.31-3.11-3.33-5.92-4.95-8.88-1.31-2.39-3.21-6.39-5.8-7.67-4.8-2.35-11.8 3.16-8.62 8.23 1.28 2 4.53.16 3.24-1.89v-.05z"/><path d="M400.45 65.22c.1-4.24 3-2.37 4.21-1.42a12.57 12.57 0 0 1 2.52 3.2 47.19 47.19 0 0 1 4.09 8.12c2.54 6.34 2.84 12.42 2 19.23-.29 2.39 3.46 2.37 3.75 0a42.41 42.41 0 0 0-6.34-28.81c-1.58-2.53-3.85-6-7.05-6.52-3.65-.63-6.85 2.79-6.93 6.21-.06 2.41 3.69 2.41 3.75 0v-.01zM311 120.68h66.46a1.88 1.88 0 0 0 0-3.75H311a1.88 1.88 0 0 0 0 3.75z"/><path d="M311 118.81c.69-8.07 4.18-17.53 20.39-17.53 8 0 17.18 7 18.07 16.14"/><path d="M312.9 118.81c.71-7.27 4.22-13.15 11.71-14.94 10.41-2.49 21.45 2.77 23.07 14 .34 2.38 4 1.37 3.62-1-1.83-12.74-14.51-19.25-26.39-16.94-9.86 1.91-14.82 9.26-15.76 18.83-.23 2.4 3.52 2.38 3.75 0v.05zM349.93 134.1c1.61 5.42-1 10.41-5.35 13.62-1.93 1.41-.06 4.67 1.89 3.24 5.73-4.2 9.19-10.73 7.07-17.86-.69-2.31-4.31-1.32-3.62 1h.01z"/><path d="M349 122.14c4 2.33 8.32 4.45 12 7.26v-3.24a162.026 162.026 0 0 1-9.32 5.41c-2.13 1.14-.24 4.38 1.89 3.24 3.167-1.7 6.273-3.503 9.32-5.41a1.9 1.9 0 0 0 0-3.24c-3.71-2.8-8-4.93-12-7.26-2.09-1.21-4 2-1.89 3.24zM337.55 120.61c.172.967.266 1.947.28 2.93v1.15c.227-.727.083-.973-.43-.74-.6-.18-2.06.39-2.69.49-.63.1-1.4.18-2.09.32-3 .58-4.71 2.2-6 5-1 2.18 2.25 4.09 3.24 1.89a4.88 4.88 0 0 1 4.37-3.34 33.74 33.74 0 0 0 5.27-.94c3.12-1 2.07-5.23 1.64-7.71-.43-2.48-4-1.37-3.62 1l.03-.05zM344.37 125.77v.9a1.88 1.88 0 1 0 3.75 0v-.9a1.88 1.88 0 0 0-3.75 0z"/><path d="M314.42 151.31c-5.51.34-11 .92-16.47 1.4-6.39.56-13.18.69-18.1-4.11-10.31-10.06-5.81-27 3.15-36.2 6.15-6.3 13.94-2.42 19.47 2.54a120.21 120.21 0 0 0 13.29 10.32c5 3.35 10.35 6.25 15.14 9.87 5.55 4.18 9.15 10.37 13 16 1.36 2 4.61.1 3.24-1.89-4.29-6.21-8.22-12.81-14.39-17.35-6.47-4.77-13.62-8.55-20.09-13.33-6.28-4.63-11-10.67-18.79-12.75-7.11-1.89-11 1-15.69 6.1-8.81 9.41-11.17 26.63-3 37.1 9.35 12 26.25 6.86 39.21 6.06 2.4-.15 2.41-3.9 0-3.75l.03-.01z"/><path d="M347.14 150c-8.14.08-16.267.363-24.38.85-2.4.14-2.41 3.89 0 3.75 8.12-.487 16.247-.77 24.38-.85 2.41 0 2.42-3.77 0-3.75zM306.93 184.86c3.22 3 3.4 6.67 1.91 10.62-.85 2.26 2.77 3.24 3.62 1 1.94-5.15 1.23-10.42-2.87-14.27-1.76-1.65-4.42 1-2.65 2.65h-.01zM288.94 211.08a48.45 48.45 0 0 0 13.65-3.18c2.79-1 6.7-2.07 8.54-4.59 1.42-2-1.83-3.83-3.24-1.89-1.41 1.94-6.17 2.85-8.18 3.51a42.18 42.18 0 0 1-10.77 2.4c-2.4.12-2.42 3.88 0 3.75zM226.05 162.86c-4.973 4.18-9.923 8.387-14.85 12.62-5 4.28-10.49 8.38-14.88 13.31-7.86 8.82-.6 17.43 7.24 22.84 2 1.37 3.87-1.87 1.89-3.24-4.56-3.15-11.29-8.23-8.14-14.69 2-4.1 7.06-7.44 10.44-10.34 6.953-6 13.933-11.95 20.94-17.85 1.85-1.56-.82-4.2-2.65-2.65h.01zM269.93 127.78c-12.61 9.37-24.79 19.25-36.83 29.35-1.85 1.55.81 4.19 2.65 2.65 11.79-9.9 23.71-19.58 36.07-28.76 1.91-1.42 0-4.68-1.89-3.24z"/><path d="M168.78 139.22c27.37-25.15 68.78-22 101.73-12.19 2.32.69 3.31-2.93 1-3.62-34.32-10.21-77-12.94-105.38 13.16-1.78 1.64.88 4.28 2.65 2.65zM137.84 183.16c1.55-8.09 6.36-14.52 10.82-21.24A89.8 89.8 0 0 1 161.53 146c1.75-1.67-.9-4.32-2.65-2.65A94.08 94.08 0 0 0 145.43 160c-4.64 7-9.59 13.74-11.2 22.13-.45 2.36 3.16 3.37 3.62 1l-.01.03z"/><path d="M134.95 185.32a410.89 410.89 0 0 0 20.89 11.55 1.9 1.9 0 0 0 2.57-.67c5.9-11.85 14.41-22.53 26.06-29.15a45.31 45.31 0 0 1 42-1.24c2.16 1.06 4.06-2.18 1.89-3.24a49.23 49.23 0 0 0-43-.2c-13.5 6.6-23.53 18.69-30.13 31.94l2.57-.67a410.89 410.89 0 0 1-20.89-11.55c-2.08-1.23-4 2-1.89 3.24l-.07-.01z"/><path d="M143.35 167.43l21.89 12.49c2.1 1.2 4-2 1.89-3.24l-21.89-12.49c-2.1-1.2-4 2-1.89 3.24z"/><path d="M151.68 192.2l10.14-15.26c1.34-2-1.91-3.9-3.24-1.89l-10.14 15.26c-1.34 2 1.91 3.9 3.24 1.89zM144.15 186.67c3-5.12 6.14-10.1 9.27-15.11 1.28-2.05-2-3.94-3.24-1.89-3.13 5-6.31 10-9.27 15.11-1.21 2.09 2 4 3.24 1.89zM145.31 205.46c.72 3.82 3.77 22.75 11.3 16 3-2.67 2.78-7.57 2.38-11.16-.33-2.91-.93-5.7-.77-8.64.13-2.47.37-4.47-.46-6.83-.83-2.36-4.42-1.29-3.62 1 1 2.75.16 5.37.28 8.2.12 2.83.9 5.43 1 8.17 0 1 .36 3.74-.3 4.4-2.86 2.91-3.68-1.67-4.15-3.11a72.75 72.75 0 0 1-2.08-9c-.45-2.37-4.06-1.37-3.62 1l.04-.03z"/><path d="M145.25 205.15c-1.94 6.41-4.08 13.71-7.86 19.3-1 1.47-2.44 3.47-3.83 1.79-1.39-1.68-.17-4.61.42-6.25 1.28-3.58 3.26-7.05 4.88-10.49 1-2.17-2.21-4.08-3.24-1.89-2.28 4.82-6.53 10.84-6.33 16.37.11 2.89 1.32 6.95 4.76 7.15 3.74.22 6.32-4.15 7.76-7a107.63 107.63 0 0 0 7.05-18c.7-2.31-2.92-3.3-3.62-1l.01.02z"/><path d="M129.76 220.44c-1.19 2.85-5.46 5.07-5.28.27a16.62 16.62 0 0 1 1.72-5.92 62.47 62.47 0 0 1 5.94-9.89c1.37-2-1.88-3.87-3.24-1.89-3.73 5.43-10.89 14.86-7.45 21.95 3.11 6.4 10.14.73 11.92-3.52.93-2.22-2.7-3.19-3.62-1h.01z"/><path d="M122.08 215c-.64 1.35-3.92 5.54-5.72 2.39-.37-.65.57-2.69.82-3.43a54.3 54.3 0 0 1 3.82-8.56c4.1-7.52 10.12-13.72 15.33-20.45 1.46-1.88-1.17-4.56-2.65-2.65-7.74 10-17 19.34-20.51 31.83-.89 3.15-.81 6.33 2.78 7.56 4.32 1.48 7.58-1 9.33-4.75 1-2.17-2.21-4.08-3.24-1.89l.04-.05zM287.34 210.16a26.15 26.15 0 0 1 .48 8.33c-.25 2.4 3.5 2.38 3.75 0a29.3 29.3 0 0 0-.61-9.32c-.54-2.35-4.16-1.36-3.62 1v-.01z"/><path d="M289.47 217.68h-83.3a1.88 1.88 0 0 0 0 3.75h83.3a1.88 1.88 0 0 0 0-3.75z"/><path d="M202.44 211.06c0 2-.07 4 0 5.93a4.13 4.13 0 0 0 1.72 3.64c2 1.38 3.86-1.87 1.89-3.24.43.3.16.07.13-.4v-5.92a1.88 1.88 0 0 0-3.75 0l.01-.01z"/><path d="M205.11 211.08h83.83a1.88 1.88 0 0 0 0-3.75h-83.83a1.88 1.88 0 0 0 0 3.75z"/><path d="M209.21 210.8v6.76a1.88 1.88 0 0 0 3.75 0v-6.76a1.88 1.88 0 0 0-3.75 0zM217.16 210.8v7.2a1.88 1.88 0 0 0 3.75 0v-7.2a1.88 1.88 0 0 0-3.75 0zM225 210v7.83a1.88 1.88 0 0 0 3.75 0V210a1.88 1.88 0 0 0-3.75 0zM232.55 210.67v7.39a1.88 1.88 0 0 0 3.75 0v-.36a1.88 1.88 0 0 0-3.75 0v.36a1.88 1.88 0 0 0 3.75 0v-7.39a1.88 1.88 0 0 0-3.75 0zM240.38 210.53v7.3a1.88 1.88 0 0 0 3.75 0v-7.3a1.88 1.88 0 0 0-3.75 0zM248.07 210.4v7a1.88 1.88 0 0 0 3.75 0v-7a1.88 1.88 0 0 0-3.75 0zM256.83 210v7.69a1.88 1.88 0 0 0 3.75 0V210a1.88 1.88 0 0 0-3.75 0zM264.78 210.67v7.16a1.88 1.88 0 0 0 3.75 0v-7.16a1.88 1.88 0 0 0-3.75 0zM272.61 210.53v7.56a1.88 1.88 0 0 0 3.75 0v-7.56a1.88 1.88 0 0 0-3.75 0zM279.51 210.53v7.16a1.88 1.88 0 0 0 3.75 0v-7.16a1.88 1.88 0 0 0-3.75 0zM316.53 339.38c2.43 0 6.68-.61 7.74 1.76 1.65 3.69-4.5 7.73-6.4 10.09-3 3.77-2.26 7.7 1.17 10.81 10.32 9.35 34.31 3.4 35.57 21.81.4 5.82-3.54 7.84-8.5 9.07-4.35 1.08-8.85 2.09-13.36 1.78-4.81-.33-9.53-2-14.31-2.61a71.6 71.6 0 0 0-11.45-.64c-8.17.21-15.86 2-23.63 4.46-7.26 2.26-20.26 4.76-22.42-5.7-1.86-9 8.28-18 12-25.53a73.8 73.8 0 0 0 4.4-11.68c1.56-5.29 3.17-11.35 9.22-12.89 2.34-.59 1.35-4.21-1-3.62-8.87 2.25-10.34 11.27-12.84 18.78a63 63 0 0 1-7.35 15c-2.77 4.17-6.06 8.08-7.57 12.94-2.18 7 .25 14 7.07 17.08 8.46 3.8 17.21-.29 25.46-2.57 12.21-3.38 24-2.75 36.26-.21 8.29 1.72 18.37 1.14 26.08-2.77 6.66-3.38 6.84-11.26 4-17.42-3.79-8.26-11.26-10.73-19.62-12.14-4.57-.78-13.15-2.09-15.95-6.23-3.12-4.63.67-5.88 3.15-8.61a17 17 0 0 0 3.29-5.64c3.06-8.37-4.62-9-11-9a1.88 1.88 0 0 0 0 3.75l-.01-.07z"/><path d="M258.51 391c8.1 2.73 16 2.46 24.09-.1 9-2.83 17.64-5.21 27.12-5.63 9.07-.4 17.64 1.54 26.6 2.28 6.81.57 13.39-.53 18.67-5.11 1.83-1.58-.84-4.23-2.65-2.65-7 6.06-16.39 4-24.69 2.77a91.47 91.47 0 0 0-21.6-.8 93.2 93.2 0 0 0-21.08 4.4c-8.76 2.88-16.47 4.26-25.46 1.23-2.29-.77-3.28 2.85-1 3.62V391zM328 366.26c-.45 2.2-1.51 4.25-1.8 6.48l.55-1.33h2.65l-.13-.14c-1.59-1.82-4.24.84-2.65 2.65l.13.14a1.93 1.93 0 0 0 2.65 0c.238-.23.41-.52.5-.84-.18.65.22-1.08.06-.43.16-.68.43-1.34.61-2 .33-1.19.81-2.33 1.06-3.54.48-2.36-3.13-3.36-3.62-1l-.01.01zM334.27 367.86l-1.33 5.84c-.53 2.35 3.08 3.35 3.62 1l1.33-5.84c.53-2.35-3.08-3.35-3.62-1zM340.41 368.94l-1.23 5.86c-.49 2.35 3.12 3.36 3.62 1l1.23-5.86c.49-2.35-3.12-3.36-3.62-1zM179.48 259.73c-3.47 6.22-10 16.41-10.48 18-5.63-3.68-12.62-6.77-18.41-10-11 7.94-19.52 14.68-30.77 22 1.69 1.11 4.31 3.68 5.84 4.69 12.3 8.1 37.85 24.19 46 25.22 12.76 1.62 30.95-16.51 38.89-26.54-10.69-10.72-20.04-23.52-31.07-33.37z"/><path d="M177.86 258.78c-3.42 6.07-8.05 11.94-10.67 18.41l2.75-1.12c-5.91-3.74-12.27-6.63-18.41-10a1.86 1.86 0 0 0-1.89 0c-10.22 7.39-20.22 15.08-30.77 22a1.9 1.9 0 0 0 0 3.24c8 5.82 16 11.35 24.53 16.49 7.77 4.68 15.86 9.83 24.5 12.77 7.81 2.65 15.59-1 22-5.41a101.92 101.92 0 0 0 21.91-20.78 1.92 1.92 0 0 0 0-2.65c-10.66-10.84-19.77-23.13-31-33.37-1.78-1.62-4.44 1-2.65 2.65 11.28 10.24 20.39 22.53 31 33.37v-2.65a100.24 100.24 0 0 1-20.59 19.81c-7.21 5-14.24 7.89-22.57 4.28-16.21-7-31.07-17.45-45.28-27.75v3.24c8-5.22 15.6-10.87 23.26-16.51 1.46-1.08 4.21-4 6-4.36 2.08-.41 3.86 1.08 5.76 2.05 4.18 2.13 8.35 4.26 12.32 6.77a1.9 1.9 0 0 0 2.75-1.12c2.48-6.12 7-11.76 10.29-17.51 1.18-2.06-2.1-3.95-3.24-1.85zM26.89 281.16c6.4 3.76 14.59 2.75 21.15-.16a32 32 0 0 0 9-6.22c4.16-3.95 9.07-9.07 15.3-5.76 5.91 3.14 8.79 12.76 16.39 13.28 6.41.44 7.8-7.72 8-12.47.09-2.41-3.66-2.41-3.75 0-.06 1.7.05 3.93-.81 5.48-2.08 3.72-5 2.79-7.82.41a51.37 51.37 0 0 1-5.53-6c-3.33-3.81-7.9-6.26-13.07-5.25-5.6 1.1-8.91 5.8-13 9.26-6.18 5.18-16.49 8.6-23.94 4.23a1.88 1.88 0 0 0-1.89 3.24l-.03-.04zM114.9 250.36c-.94-3.38-2.06-6.7-4.88-9-3.33-2.68-7.95-3.37-11.64-5.39a25.31 25.31 0 0 1-10.75-10.62c-1.79-3.61-3.14-7.05-6.37-9.64-1.87-1.49-4.54 1.14-2.65 2.65 3.44 2.75 4.34 6.48 6.42 10.14a26.63 26.63 0 0 0 6.58 7.42 34.16 34.16 0 0 0 9 5.18c5.69 2.25 9 3.93 10.71 10.22.65 2.32 4.27 1.34 3.62-1l-.04.04z"/><path d="M88.48 231.88c-2.08 5.71-4 11.85-1.61 17.79 2.18 5.54 7.09 9.71 11.81 13.08 2 1.4 3.84-1.85 1.89-3.24-3.79-2.7-7.86-6.09-9.88-10.39-2.51-5.37-.48-11 1.41-16.24.83-2.27-2.8-3.25-3.62-1zM29.62 278c.95-6 2.89-12.11 7-16.66 2.57-2.86 5.84-5 8.93-7.26a82 82 0 0 0 12-10.88 38.56 38.56 0 0 0 5.07-6.77c1.55-2.74 2.57-5.76 4.16-8.47 2.91-5 6.76-8.43 12.26-10.09l1.18-.57c-2.84-2-5.12-3.57-8.44-3.37a17.76 17.76 0 0 0-10.59 5.15c-2.27 2.6-3.87 5.71-5.66 8.66a75.35 75.35 0 0 1-12.7 15.76c-3.89 3.68-8.18 6.95-11.82 10.88-3.64 3.93-6.67 8.69-7.31 14-.45 3.69.42 7.73 3.12 10.28.34.32.705.615 1.09.88"/><path d="M31.43 278.48c1.41-8.13 4.36-14.76 11-19.88 5.54-4.24 11-8.26 15.72-13.43 4.4-4.78 6.67-10 9.76-15.59 3.19-5.8 7.47-7.91 13.14-10.68 1.37-.67 1.06-2.5 0-3.24-6.43-4.51-13.09-4.56-19.44.39-3.53 2.76-5.52 7-7.8 10.72a75 75 0 0 1-10 13c-6.61 6.84-15.25 12.14-19.66 20.84-3.35 6.6-3.91 15.2 2.33 20.24 1.86 1.5 4.53-1.13 2.65-2.65-7.4-6-2.5-16.17 2.57-21.92 4.84-5.5 11-9.7 15.92-15.09a94.43 94.43 0 0 0 11.19-15.51c2-3.31 3.93-6.3 7.44-8.17 4.92-2.61 8.53-1.7 12.92 1.38v-3.24c-5 2.46-9.15 4.26-12.57 9-2.12 2.93-3.32 6.24-5 9.43a41.84 41.84 0 0 1-7.36 9.75c-5.46 5.67-12.34 9.52-17.9 15-5.1 5-7.4 11.76-8.59 18.67-.41 2.36 3.2 3.37 3.62 1l.06-.02z"/><path d="M56.91 245.55a32.71 32.71 0 0 0 31-3.26c2-1.35.11-4.6-1.89-3.24a29.3 29.3 0 0 1-28.16 2.88c-2.23-.92-3.2 2.71-1 3.62h.05zM58.65 260.83a33.36 33.36 0 0 1 3 5.92c.85 2.23 4.48 1.26 3.62-1a38.23 38.23 0 0 0-3.37-6.81c-1.28-2-4.53-.16-3.24 1.89h-.01zM51.43 264.64a121.685 121.685 0 0 1 3.76 7.07c1.07 2.16 4.31.26 3.24-1.89a140.505 140.505 0 0 0-3.76-7.07c-1.2-2.1-4.44-.21-3.24 1.89zM44.91 269.39A213.674 213.674 0 0 1 49 276.1c1.19 2.1 4.43.21 3.24-1.89a124.72 124.72 0 0 0-4.07-6.71c-1.31-2-4.56-.14-3.24 1.89h-.02zM95.27 266.07c-6.1-6.21-18-9.7-23.68-1.06-1.32 2 1.92 3.91 3.24 1.89 4.26-6.52 13.55-2.5 17.79 1.82 1.69 1.73 4.34-.93 2.65-2.65zM95.46 238.18a23 23 0 0 0 9.86 19.5c2 1.4 3.84-1.85 1.89-3.24a19.12 19.12 0 0 1-8-16.26c.06-2.41-3.69-2.41-3.75 0zM41.56 201.68a7.74 7.74 0 0 0-.74 12.24 8.64 8.64 0 0 0 7 1.88 10.76 10.76 0 0 0 6.33-3.66 8.37 8.37 0 0 0 1.51-2.44c1.2-3.14-.44-7-3.34-8.68a10.15 10.15 0 0 0-9.67 0"/><path d="M40.23 200.35a9.7 9.7 0 0 0-1.6 14c3.82 4.44 10.47 4.26 14.9.89 4-3 5.7-8.08 3-12.58-3.18-5.25-9.74-5.53-14.82-3.22-2.19 1-.29 4.23 1.89 3.24 3.27-1.49 7.57-1.43 9.69 1.88 2 3.06.25 6.26-2.49 8.08-6.81 4.54-15-4-7.89-9.62 1.9-1.49-.77-4.12-2.65-2.65l-.03-.02zM20.72 221.86a10.1 10.1 0 0 0-3.9 6.65 9.09 9.09 0 0 0 .94 6.25C19.2 237 22 238 24.6 238a11 11 0 0 0 7.69-2.82 7.14 7.14 0 0 0 2-3.21c.63-2.27-.2-4.7-1.31-6.78A9.22 9.22 0 0 0 29.2 221a11.17 11.17 0 0 0-3.8-.83 9.56 9.56 0 0 0-4.68 1.69"/><path d="M19.4 220.54c-4.15 3.68-6.53 10.63-3 15.55 3.53 4.92 11 4.71 15.56 1.69 5.58-3.68 5.13-10.12 1.62-15.18-3.25-4.68-9.21-5.36-13.85-2.35-2 1.31-.14 4.56 1.89 3.24 2.73-1.77 6.08-2 8.42.62 3.17 3.53 3.53 8.8-1.13 11.07-3.1 1.51-8 1.5-9.74-1.92-1.86-3.58.08-7.64 2.83-10.07 1.81-1.6-.85-4.25-2.65-2.65h.05zM5.91 243.85a7.72 7.72 0 0 0-3.41 4.69 9.21 9.21 0 0 0-.44 4c.5 3.66 4.3 6.4 8 6.12a9.44 9.44 0 0 0 8-6.75 6.24 6.24 0 0 0 .27-2.67A6.53 6.53 0 0 0 17 246.3a9.07 9.07 0 0 0-5.47-3.66 6.09 6.09 0 0 0-5.57 1.2"/><path d="M5 242.23c-4.46 2.94-6.47 9.72-3.22 14.27a9.75 9.75 0 0 0 13.11 2.41c4.55-2.86 7-8.88 3.72-13.54-3.12-4.45-9-6.21-13.61-3.13-2 1.33-.12 4.58 1.89 3.24 2.81-1.87 5.95-1.12 8.16 1.42 2.55 2.93 1 6.66-1.88 8.64a6 6 0 0 1-7.9-.66c-2.48-2.69-1.28-7.49 1.62-9.4 2-1.32.13-4.57-1.89-3.24v-.01zM47.37 217.3a123 123 0 0 1-.16 17.1c-.19 2.41 3.56 2.39 3.75 0a123 123 0 0 0 .16-17.1c-.14-2.4-3.9-2.41-3.75 0z"/><path d="M34 236.38c5.113 0 10.22-.147 15.32-.44 2.4-.15 2.41-3.9 0-3.75-5.107.313-10.213.46-15.32.44-2.41 0-2.42 3.74 0 3.75z"/><path d="M31.71 235.2c.34 5.23.377 10.476.11 15.71-.12 2.41 3.63 2.41 3.75 0 .267-5.234.23-10.48-.11-15.71-.16-2.4-3.91-2.41-3.75 0z"/><path d="M18.89 254.41a77.94 77.94 0 0 0 16-2.78c2.32-.66 1.33-4.28-1-3.62a73.07 73.07 0 0 1-15 2.65c-2.39.16-2.41 3.92 0 3.75zM112.69 250.09l-1.09-.09L96 263.51a5.2 5.2 0 0 0-1.5 1.77c-1 2.26 1.55 2.33.56 6.76L95 273c10.42 7.36 24.1 16.29 35 23.41 11.25-7.32 21.61-14.31 32.62-22.25a495 495 0 0 0-49.93-24.07z"/><path d="M112.69 248.22c-2.88-.36-4.64 2.34-6.67 4.11-3.31 2.88-6.65 5.72-9.94 8.63-1.79 1.58-3.83 3.08-3.65 5.72.122.515.272 1.022.45 1.52.24 1.37 0 3 .26 4.31.49 2.14 1.55 2.55 3.46 3.88 3.68 2.56 7.387 5.083 11.12 7.57 7.06 4.73 14.17 9.38 21.28 14a1.84 1.84 0 0 0 1.89 0c11-7.19 21.93-14.56 32.62-22.25a1.9 1.9 0 0 0 0-3.24 522.93 522.93 0 0 0-50.32-24.24c-2.23-.93-3.2 2.7-1 3.62a514 514 0 0 1 49.42 23.86v-3.24a805.027 805.027 0 0 1-24.72 17.06c-1.58 1-4.46 3.88-6.3 4.15-1.84.27-3.57-1.49-5.11-2.5a1547.477 1547.477 0 0 1-12.24-8.07c-3.82-2.54-7.623-5.103-11.41-7.69-1-.67-4.42-2.37-4.8-3.09-.17-.33.16-2.28 0-3.19-.21-1.5-.76-1.86-.23-3.32.78-2.14 3.73-3.87 5.43-5.35l5.77-4.87c1-.91 3.34-3.8 4.73-3.63 2.35.3 2.33-3.46-.04-3.75zM357.3 253.6c-3-7-13.12-11.54-19.61-15.41-6.49-3.87-41-16.52-48.22-18.64l-85.32.37c-2.61 1.81-4.41 4.55-6.06 7.26-6.81 11.16-14.45 21.93-19.21 34 11 9.85 21 21.17 31.65 31.9l.49-.6c9.46-12.25 24.22-35 25.49-37.24 21.38 6.62 58 18.29 80.1 21.53-9.22 20.17-24.36 39.37-30.14 60.79l-.43 1.5a816.377 816.377 0 0 1 30.5-1.57l2.4-.51a467.6 467.6 0 0 1 26.12-40.1 118.42 118.42 0 0 0 9-13.33c1.34-2.5 3.05-5.22 3.78-8 .68-2.57 1.4-2.62 1.49-5.28.17-6.27.44-10.88-2.03-16.67z"/><path d="M358.92 252.65c-4-8.11-13.9-12.81-21.55-16.78-6.28-3.26-13.09-5.68-19.68-8.21-7.77-3-15.57-5.88-23.45-8.55a22 22 0 0 0-8-1.43l-76.48.33c-4.86 0-6.86-.38-10.31 3.86-4.41 5.42-7.82 12-11.48 18a141.66 141.66 0 0 0-10.88 20.85 1.82 1.82 0 0 0 .48 1.82c11.13 10 21.11 21.25 31.65 31.9a1.89 1.89 0 0 0 2.65 0 315.77 315.77 0 0 0 26.27-38.22l-2.12.86c26.27 8.14 52.83 17.41 80.1 21.53l-1.12-2.77c-9.87 21.24-24.25 40-30.75 62.73a1.89 1.89 0 0 0 1.81 2.37c6.667-.46 13.333-.847 20-1.16 3.86-.18 9.79.82 13.39-1 1.48-.74 2-2.39 2.83-3.78 1.64-2.773 3.307-5.53 5-8.27a439.277 439.277 0 0 1 10.35-16c11.51-17.09 30.23-35.27 21.49-57.61-.87-2.22-4.5-1.26-3.62 1 3.89 9.95 2 19.48-3.1 28.51-5.2 9.22-12.1 17.44-18 26.21a471.312 471.312 0 0 0-8.44 13 590.649 590.649 0 0 0-4.24 6.86c-1 1.67-2.23 5-3.6 6.27-1.13 1.07-3.74.77-5.39.84-2.86.113-5.72.237-8.58.37-6.04.293-12.073.65-18.1 1.07l1.81 2.37c6.41-22.44 20.64-40.9 30.38-61.84.45-1 .18-2.56-1.12-2.75-27.27-4.12-53.83-13.39-80.1-21.53a1.9 1.9 0 0 0-2.12.86 309.46 309.46 0 0 1-25.68 37.46h2.65c-5.3-5.36-10.46-10.85-15.69-16.28-2.59-2.69-5.2-5.37-7.86-8a165.779 165.779 0 0 0-4-3.87c-2.22-2.09-3.17-2.4-2.06-5.56 2.19-6.26 6.42-12.25 9.9-17.85 1.89-3 3.82-6 5.7-9.07 1.4-2.25 4-8.21 6.37-9.15 1.93-.77 6-.24 8.44-.25l19.44-.08 44.8-.19c3.88 0 8-.45 11.85-.05 4.33.45 9.54 3 13.84 4.59 9.51 3.44 19 6.94 28.33 11a97.84 97.84 0 0 1 9.15 4.7c5.8 3.28 12.57 6.58 15.63 12.84 1.03 2.12 4.29.22 3.21-1.95z"/><path d="M298.64 338.34c-1.74 6.14-2.92 11.58-8.72 15.2a27.91 27.91 0 0 1-7.5 3.09c-2 .54-5.71 2.07-7.7 1.49l.83 3.13.17-.17h-2.65l.12.12c1.68 1.74 4.33-.91 2.65-2.65l-.12-.12a1.9 1.9 0 0 0-2.65 0l-.17.17a1.9 1.9 0 0 0 .83 3.13c2.83.82 6.94-.73 9.69-1.49a31.47 31.47 0 0 0 9-3.85c6.33-4.16 7.85-10.11 9.82-17.05.66-2.33-3-3.32-3.62-1h.02z"/><path d="M289.79 339c-2.43 5.46-5.69 10-12.25 9.76-2.41-.07-2.41 3.68 0 3.75 7.85.23 12.51-4.94 15.49-11.62 1-2.19-2.26-4.1-3.24-1.89zM273.32 362.46c5.44.85 10.61 3.83 14.73 7.45 4.12 3.62 5 8.81 5.73 14.06.32 2.38 3.94 1.37 3.62-1-.8-5.92-2-11.62-6.7-15.71-4.7-4.09-10.28-7.46-16.38-8.41-2.36-.37-3.38 3.24-1 3.62v-.01zM271.11 410.51a10.07 10.07 0 0 0 2.64 15.82 9.34 9.34 0 0 0 5.6 1 9.75 9.75 0 0 0 4.57-2 10.85 10.85 0 0 0 4.07-9.93 10 10 0 0 0-7-8c-3.61-1-7.8 0-9.9 3.11"/><path d="M269.78 409.19c-4.28 5.13-4.41 12.49.74 17.16a11.43 11.43 0 0 0 16.48-1.42 12.22 12.22 0 0 0-.82-16.66c-4.62-4.38-12.42-4.09-16.41.92-1.49 1.87 1.15 4.54 2.65 2.65 6.22-7.83 17.42 1.47 12.45 9.63-6.08 10-19.73-.91-12.45-9.63 1.54-1.84-1.1-4.51-2.65-2.65h.01zM305.42 406.64c-2.56 1.14-4.17 3.73-5.14 6.35-.97 2.62-1.41 5.43-.44 8a8.77 8.77 0 0 0 7.8 5.3 12.29 12.29 0 0 0 12.08-11 8.65 8.65 0 0 0-2.86-7.1 11 11 0 0 0-5.39-2.2c-2.68-.43-5.71-.19-7.65 1.71"/><path d="M304.47 405c-5.67 3.11-9.13 12-5.78 17.89 3.46 6.09 11.58 6.43 16.87 2.72 5.29-3.71 8.14-11.52 4.05-17.17-3.75-5.18-12.26-6-17.12-2.12-1.88 1.51.79 4.15 2.65 2.65 3.68-3 10.83-1.07 12.35 3.4 1.4 4.13-1.32 8.62-4.87 10.69-3.55 2.07-8.54 1.77-10.69-2.06-2.33-4.16.59-10.65 4.44-12.76 2.12-1.16.23-4.4-1.89-3.24h-.01zM333.21 409.48a13.16 13.16 0 0 0-1.93 7.09 7.91 7.91 0 0 0 3.43 6.3 8.36 8.36 0 0 0 3.13 1.11 10.8 10.8 0 0 0 10.16-3.84 9.12 9.12 0 0 0 .3-10.61 10.25 10.25 0 0 0-5.32-3.58 9.73 9.73 0 0 0-6.13 0 6.31 6.31 0 0 0-4.07 4.39"/><path d="M331.59 408.53c-2.88 5.48-3.41 13.37 3.16 16.46a12.53 12.53 0 0 0 15.42-4.77 10.78 10.78 0 0 0-3.78-14.8c-5.51-3.24-13.29-2.26-15.45 4.38-.75 2.3 2.87 3.29 3.62 1 2.82-8.7 19.27.43 11.42 8.79a9 9 0 0 1-8.73 2.34c-5.38-1.58-4.41-7.71-2.42-11.51 1.12-2.14-2.12-4-3.24-1.89zM285.21 408.94L299 397.32h-2.65a567.834 567.834 0 0 1 8.7 8.77c1.68 1.74 4.33-.92 2.65-2.65a567.834 567.834 0 0 0-8.7-8.77 1.92 1.92 0 0 0-2.65 0l-13.83 11.62c-1.85 1.55.81 4.2 2.65 2.65h.04z"/><path d="M310.09 404.77l10.63-8.66c1.87-1.52-.8-4.16-2.65-2.65l-10.63 8.66c-1.87 1.52.8 4.16 2.65 2.65z"/><path d="M319.49 397.77c2.6.87 4.86 2.8 7.1 4.33l3.49 2.37c.84.57 2.89 1.45 3.45 2.24l.29-2.27-.09.07h2.65l-.07-.06c-1.85-1.52-4.52 1.12-2.65 2.65l.07.06a2 2 0 0 0 2.65 0l.09-.07a1.87 1.87 0 0 0 .29-2.27c-1.68-2.36-5.4-4-7.78-5.62-2.72-1.85-5.35-4-8.5-5-2.29-.77-3.28 2.85-1 3.62l.01-.05zM315.16 337.94c-2.34 4.72-6 8.65-5.57 14.31.41 4.85 4.37 8.22 8.9 9.22 2.35.52 3.36-3.1 1-3.62-4.9-1.08-7.29-5.16-5.52-10.12 1-2.77 3.11-5.25 4.42-7.9 1.07-2.16-2.16-4.06-3.24-1.89h.01zM138.65 200.6c2.947 1.793 5.8 3.723 8.56 5.79 1.93 1.45 3.8-1.81 1.89-3.24a104.146 104.146 0 0 0-8.56-5.79c-2.07-1.26-4 2-1.89 3.24z"/><path d="M263 397.05c4.13.82 8.304 1.408 12.5 1.76 1.91.16 2.47-2.61.95-3.49-4.29-2.49-10-2.44-13.67-5.84l-1.82 3.13a63.43 63.43 0 0 1 8 3.59l1.44-3.43a55.18 55.18 0 0 1-6.93-2.47l-.95 3.49c12.37.6 24.3-2.34 36.17-5.45 2.37-.62 1.34-4-1-3.62l-22.19 3.88c-2.4.42-1.34 3.94 1 3.62l25.95-3.62-1-3.62-27 7.34c-2.35.64-1.35 4.07 1 3.62l21.65-4.22-1-3.62-23.73 5.06 1 3.62a153.25 153.25 0 0 1 24-5.19l-.5-3.68a158.06 158.06 0 0 1-24.58 6.44c-2.06.37-1.61 3.92.5 3.68a127.33 127.33 0 0 0 25.43-5.5 1.88 1.88 0 0 0-1-3.62 149.53 149.53 0 0 0-24.08 5.54c-2.29.73-1.34 4.26 1 3.62a689.06 689.06 0 0 1 42.83-10.35l-1-3.62a115.68 115.68 0 0 1-22.27 3.49c-2.38.19-2.45 3.72 0 3.75 9.827.107 19.65.047 29.47-.18v-3.75c-8.947.353-17.89.29-26.83-.19V391c8.1-.82 19.28-3 27.08-.12v-3.62a53.24 53.24 0 0 1-23.33-.73l-.5 3.68a79.48 79.48 0 0 1 26.36 1.1l.5-3.68a81 81 0 0 1-27.9-1.94v3.62c9.45-1.56 18.9-2.9 28.13.41l.5-3.68a116 116 0 0 1-25.76-1.04l-.5 3.68c7.78 0 15.5 0 23.13 1.72l.5-3.68a462.094 462.094 0 0 0-34 2.32c-2.34.25-2.47 3.77 0 3.75a130.73 130.73 0 0 0 19.64-1.7c2.1-.34 1.59-3.76-.5-3.68a92.27 92.27 0 0 0-16.31 2c-2 .44-1.62 3.83.5 3.68 13.18-.92 26.36-1.76 39.58-.95v-3.75a184.46 184.46 0 0 1-20.32-.6c-2.43-.21-2.34 3.46 0 3.75A135.53 135.53 0 0 0 342 392c2.36-.21 2.46-3.73 0-3.75l-21.65-.25v3.75l19.13-.5a1.88 1.88 0 0 0 0-3.75c-3.56-.14-13.72.73-15.81-2.72l-1.67 2.86a92.26 92.26 0 0 1 18.17 2.67l.5-3.68a284.125 284.125 0 0 0-24.42.67 1.88 1.88 0 0 0 0 3.75 68.22 68.22 0 0 1 12.58 2.87l1-3.62-11.21-1.67c-1.45-.22-3.14 1.25-2.12 2.75 3.6 5.3 14.67 4.5 20.16 4a1.88 1.88 0 0 0 .5-3.68 65 65 0 0 0-20.35-3c-2.48 0-2.33 3.45 0 3.75a154.13 154.13 0 0 0 19.79 1.29c3.53 0 8.53.93 10.76-2.42.86-1.29-.31-2.7-1.62-2.82a13.42 13.42 0 0 0-7.39 1.34c-2.08.95-.48 3.74 1.44 3.43a32.63 32.63 0 0 0 11.07-3.93c2-1.12.54-3.73-1.44-3.43a51.74 51.74 0 0 0-8.36 2l1.44 3.43 7.33-3.17c1.93-.83.53-4.21-1.44-3.43l-5.8 2.26c-2.19.87-1.38 4.3 1 3.62a21.21 21.21 0 0 0 4.92-2c1.39-.81 3.24-3 4.8-3.22l-1.87-1.87a4.88 4.88 0 0 1-3.49 4.24l2.57 2.57c1.32-2 2.7-4 4.21-5.93l-3.2-1.33c0 7.32-8.57 7.68-13.89 7.71-2.41 0-2.42 3.76 0 3.75a51.6 51.6 0 0 0 10.5-1.12c2-.43 1.62-3.93-.5-3.68l-12.54 1.48c-2.33.28-2.46 3.87 0 3.75a24.08 24.08 0 0 0 14.11-5.29c1.9-1.48-.77-4.11-2.65-2.65a19.68 19.68 0 0 1-11.46 4.19V395l12.54-1.48-.5-3.68a46.61 46.61 0 0 1-9.5 1v3.75c7.6 0 17.7-1.87 17.64-11.46 0-1.48-2.15-2.66-3.2-1.33-1.7 2.16-3.3 4.37-4.8 6.68-1.13 1.75 1 3.24 2.57 2.57 3.32-1.43 4.9-4 5.34-7.48a1.87 1.87 0 0 0-1.87-1.87c-4.05.52-6.7 4.17-10.71 5.32l1 3.62 5.84-2.31-1.45-3.44-7.33 3.17c-1.95.84-.56 4.08 1.44 3.43a51.74 51.74 0 0 1 8.36-2l-1.47-3.41a29.72 29.72 0 0 1-10.17 3.56l1.44 3.43a8.28 8.28 0 0 1 5.49-.83l-1.62-2.82c-1.21 1.82-11.11.54-13.19.46-4.73-.17-9.43-.6-14.12-1.19v3.75a61.77 61.77 0 0 1 19.35 2.86l.5-3.68a43.23 43.23 0 0 1-8.1.15c-1.94-.17-7.51-.32-8.82-2.25l-2.12 2.75 11.21 1.67a1.88 1.88 0 0 0 1-3.62 73.6 73.6 0 0 0-13.58-3v3.75a284.273 284.273 0 0 1 24.42-.67 1.88 1.88 0 0 0 .5-3.68 97.33 97.33 0 0 0-19.19-2.83 1.89 1.89 0 0 0-1.62 2.82c3.25 5.38 13.73 4.37 19 4.58v-3.75l-19.13.5a1.88 1.88 0 0 0 0 3.75L342 392v-3.75a135.53 135.53 0 0 1-28.87-.45v3.75c6.757.573 13.54.774 20.32.6a1.88 1.88 0 0 0 0-3.75c-13.21-.81-26.39 0-39.58.95l.5 3.68a86.6 86.6 0 0 1 15.32-1.9l-.5-3.68a124.08 124.08 0 0 1-18.6 1.55v3.75a459.113 459.113 0 0 1 34-2.32c2-.06 2.64-3.21.5-3.68-8-1.76-16-1.89-24.13-1.86-2 0-2.65 3.35-.5 3.68a120.3 120.3 0 0 0 26.74 1.19 1.88 1.88 0 0 0 .5-3.68c-9.83-3.52-20.07-2.07-30.13-.41-1.87.31-1.71 3.19 0 3.62a84.14 84.14 0 0 0 28.9 2.08c1.94-.2 2.73-3.21.5-3.68a82.64 82.64 0 0 0-27.35-1.23c-1.95.24-2.72 3.12-.5 3.68a57.82 57.82 0 0 0 25.33.73c2-.38 1.57-3 0-3.62-8.34-3.08-19.43-.89-28.08 0a1.88 1.88 0 0 0 0 3.75c8.94.48 17.883.543 26.83.19 2.4-.09 2.42-3.81 0-3.75-9.82.227-19.643.287-29.47.18v3.75c7.86-.63 15.65-1.49 23.27-3.63 2.34-.66 1.35-4.1-1-3.62a689.06 689.06 0 0 0-42.83 10.35l1 3.62a149.53 149.53 0 0 1 24.08-5.54l-1-3.62a122.55 122.55 0 0 1-24.43 5.36l.5 3.68a158.06 158.06 0 0 0 24.58-6.44c1.85-.65 1.73-4-.5-3.68a159.3 159.3 0 0 0-25 5.32 1.88 1.88 0 0 0 1 3.62l23.73-5.06c2.37-.51 1.36-4.08-1-3.62l-21.65 4.22 1 3.62 27-7.34c2.4-.65 1.32-3.94-1-3.62l-25.98 3.61 1 3.62 22.16-3.89-1-3.62c-11.56 3-23.13 5.9-35.18 5.32-1.77-.09-2.67 2.74-.95 3.49a62.59 62.59 0 0 0 7.83 2.85c2 .58 3.34-2.41 1.44-3.43a70.39 70.39 0 0 0-8.87-4c-1.77-.65-3.16 1.91-1.82 3.13 4.15 3.81 9.72 3.7 14.43 6.43l.95-3.49c-3.86-.324-7.7-.865-11.5-1.62-2.36-.47-3.37 3.15-1 3.62l.01.04z"/></g></svg>


--------------------------------------------------------------------------------
/assets/wouter.svg:
--------------------------------------------------------------------------------
1 | <svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1088 952"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:4px}.cls-2{fill:#fff}</style></defs><path d="M99.4 644.39C44 607.42 34.68 500.23 35 442c.54-85.13 20.63-173.91 60.81-249.26C141 108 209 58.94 304.16 54.23c61.51-3 121.26-3.29 182.72-1.73l121.57 3.09c20.94.54 42.39 1.17 61.86 8.91 33.37 13.25 55.94 45.91 66.06 80.36s9.61 71 9 106.9q-1.12 67.94-2.26 135.88c60.22.59 124-1 184.12-4.19-.65-34.45-2.64-72.51-3.29-107-6.2 24.19-24.78 45.71-48.94 52s-52.74-5.06-63.14-27.77c-6.13-13.39-5.79-28.8-4.08-43.42.92-7.92 2.39-16.23 7.52-22.35s15.05-8.87 21.21-3.81c3.53 2.9 5 7.53 6.64 11.81 4.73 12.57 14.28 25.66 27.71 25.77 11.07.09 20.18-8.89 25.63-18.52 8.48-15 11-34.4 2.48-49.37s-30.06-22.4-44.24-12.6c-9.14 6.32-14.67 18.53-25.6 20.56-9.26 1.73-18.46-5.51-21.53-14.41s-1.24-18.79 2.08-27.59c7.63-20.23 24.09-37.85 45.06-43.1s45.76 4.25 54.8 23.89c1.89 4.1-2-2.2.55 1.53 6.51-41.33-33.32-65.91-69.22-65.92-9.53 0-19.47.45-28.13-3.52-42-19.31 22.25-57.16 46.43-53.4 21.7 3.37 40.51 18.22 52.31 36.74s17.3 40.44 19.85 62.26a81.84 81.84 0 00-8.83-73.92c-5.48-8-12.83-16.66-10.65-26.12 2.72-11.77 18.51-15.27 30-11.64 17.45 5.55 31.35 21.99 36.73 39.15 3.35 10.69 2.72 23.7 3.7 34.88l3.33 37.73 6.76-64.79c.65-6.22 1.32-12.5 3.25-18.46 6-18.61 24.63-23.64 33.94-5.42 6.19 12.12 8.51 27.05 9.11 40.55q2.44 55.32 3.66 110.68 2.49 112.71 0 225.44c-.44 19.94-1 40-5.41 59.49-9.07 40.18-29.74 69.35-71 81.6 26 9.45 45.15 32.72 55.62 58.33s13.36 53.6 15.32 81.2c4.6 65.09 2.48 136.81-39.91 186.43-18.1 21.18-61.06 21.81-86.75 24a4157.15 4157.15 0 01-456.23 13.58c-14.84-.37-29.88-.85-44.12-5a88.11 88.11 0 01-51.62-41.44c-44 14.83-89.49 35.78-133.5 50.61-28.08 9.46-56.5 19-86 21.4s-60.75-3-84.33-21c-9-6.84-16.88-15.64-20.79-26.25-8.62-23.45 3.84-49.67 20.59-68.2 14.83-16.41 33-29.42 51.67-41.33 42.81-27.38 95.84-59.5 146.43-69-18.87-4.77-36.18-9.59-50-24.28a125.34 125.34 0 01-13.92-18.27c-2.13-3.3-4.16-6.66-6.15-10-.43-.72-3.86-7.44-4.23-7.43-14.27.28-28.59.56-42.83-.77a76.28 76.28 0 01-35.77-12.59z"/><path d="M530.35 44.55a24.79 24.79 0 0119.32.47 18 18 0 018.61 7.13c1.89 3.28 2.18 7.63.1 10.79-2.21 3.35-6.41 4.7-10.36 5.41-8 1.45-20.94 1.63-26.86-5.24s2.84-15.86 9.19-18.56z"/><path class="cls-1" d="M204.67 509.63c6-85.14 14.83-171.24 25.62-255.9 6.25-49 13.68-99.18 37.15-142.7 3.45-6.39 7.55-12.92 14-16.19 4.94-2.5 12.95-2.55 16.86 2 4.53 5.24 2.93 14.06 1.66 20.14-4.25 20.47-10.28 56.09-14.53 76.55 3.32-19.64 9.8-54.21 15.82-73.2C305 108.66 309 97.13 319.1 89.4c7.71-5.9 21.06-9.81 26.42 1 3.65 7.39-.76 17.46-3.88 24.27-4 8.66-6.56 17.86-9.32 27q-8.43 27.76-14.82 56.11c5.37-16 9.35-32.55 15.24-48.41 6.61-17.77 15.62-35.86 28.46-49.87 8.24-9 26.38-26.57 39.67-16.5 15.95 12.1-6.2 37.31-13.07 47.72-14.23 21.59-28 43.7-37.94 67.64 12.3-26.85 27.89-55 48.64-76.28 9.7-9.94 21.37-21 35.45-23.92 13.05-2.69 32.59 6.64 25.05 22.52-2.79 5.9-8.24 10-13.44 14-18.52 14.09-36.44 29.19-51.5 46.93-13.29 15.65-24.36 33.47-30.8 53-7.12 21.68-9.65 44.74-11.86 67.41-3.79 38.95-6.36 78.11-4.82 117.21 1.21 30.63 4.94 61.23 3.61 91.85s-8.16 61.95-26 86.87a111.66 111.66 0 01-28.64 27.76c-25.24 16.89-56.09 22.74-86.11 27.35-28.36 4.37-57.59 7.93-85.52 1.34S69.3 629 61 601.55M597.46 323.84l-3.27 80.77c-.8 19.82-1.6 39.81 1.68 59.37s10.94 38.93 25 53C646.28 542.5 686 545.38 722 546.73L940.89 555c19 .71 37.3-.62 53.77-10.49 15.15-9.07 27.86-21.87 38.42-35.9M803.15 189.63c2.65 5.13 9 7.78 14.7 6.75s10.48-5.33 12.85-10.6a26.38 26.38 0 001.15-17c-2.46-10.16-10.48-19.33-20.79-21.09M796.63 49c.13 4.28 2.89 8.16 6.42 10.59 8.77 6.05 21.82 3.44 28.88-4.54s8.42-20 5-30.09c-.65-1.91-2.1-4.14-4.07-3.71M936.23 8A19.26 19.26 0 00933 25.45a19.26 19.26 0 0012.6 12.46 23.23 23.23 0 0015.48-1.26 40.38 40.38 0 0012.81-9.15M1026.27 25.54c-6.4 5.34-11 13.83-9 21.92 1.58 6.48 7.21 11.51 13.6 13.43s13.37 1 19.6-1.36M806.57 239.72c13.1-8.58 19.36 3.92 20.81 15.52a32.83 32.83 0 01.08 9.18 21.9 21.9 0 01-29.74 17"/><path class="cls-1" d="M459.3 120.07c45.51-4 59 48.75 65.82 82.84 36.93 184.18-6.68 376.91 26.61 561.78 96.74-.76 198.22 6.94 294.74 13.52 48.88 3.33 154.15.54 159.26 68.22 4.21 55.72-66 68.75-107.37 76.85"/><path class="cls-1" d="M357.16 848c20.48 33.42 65.06 36.68 99.36 41.85 17.69 2.66 35.67 2.45 53.55 2.23l384.33-4.73c20.66-.25 41.57-.54 61.52-5.91s39-16.33 49.9-33.71M541 675.29c116.43 3.26 186.81 5 342.34 6M539 544c51.32.74 114.57 1.19 165.89 1.93M63.65 810.45a940 940 0 01139-75.37c49.29-21.59 102.28-37.16 153.5-53.76M911.65 655.94a149.89 149.89 0 00-27.91 25.21c7.48 11.27 16.76 22.06 27.39 30.44M631.11 48.32c14.66 9 28 17 35.85 33 7 14.29 8.71 30.73 7.16 46.56-.64 6.52-1.87 13.12-5 18.86-4.42 8-12.35 13.7-21 16.83s-17.89 3.89-27 3.88a82.94 82.94 0 01-1.38 21.73c-1.05 5.3-2.9 10.89-7.29 14-4.23 3-10.06 3.06-14.92 1.2s-8.91-5.38-12.53-9.06a101.39 101.39 0 01-17.52-24.25c-.23 4.27-3.19 8.67-6.87 11.6A89.28 89.28 0 01525 199.84"/><path class="cls-1" d="M517.75 58.23c2.64-9.71 10.8-16.68 21.36-16.21C551 42.55 560 52.56 567.24 62a661.65 661.65 0 0138 54.8"/><path class="cls-2" d="M622.39 112.21c-1.25.11-2.4.26-3.38.4a45.31 45.31 0 00-23.76 10.77c-4.67 4.2-9 9.91-8.21 16.15.66 5.29 5 9.61 10.05 11.36s10.6 1.28 15.75-.07c7.33-1.91 15.43-3.88 21.22-9.42 6.78-6.48 13.4-21.17 1.93-26.94a26 26 0 00-13.6-2.25zM623.32 169.38c-2.59 3.13-2.16 9.57-1.83 13.05a7.41 7.41 0 002.31 5c1.52 1.29 3.62 1.63 5.62 1.79A49.65 49.65 0 00653.9 185c5-2.29 11.89-7.35 12.55-13.37.73-6.77-7-11.16-12.46-9.56-7.7 2.24-14.46 5.89-22.77 5.63a24.57 24.57 0 00-5 0 4.79 4.79 0 00-2.9 1.68z"/><path class="cls-1" d="M620.3 144.65a8 8 0 015.27 1.79 24.14 24.14 0 019.11 26M264.72 117.08c4.93 2.47 11.22 1.51 15.56-1.87a19.13 19.13 0 007-14.43c.07-2.4-.33-5-2-6.73M308 103.64a16.18 16.18 0 0015.63 5.52 17.05 17.05 0 004.92-1.91 18.59 18.59 0 007.74-23.65M367.87 93a10.94 10.94 0 005.37 5.34 19.93 19.93 0 0024-5.17c2.18-2.75 3.61-6.4 2.64-9.77M196.88 766.45c20.56 26.08 39.8 53.71 51.69 84.71M255.29 737.75A285.07 285.07 0 01305 829.63M310.66 713.63a812.1 812.1 0 0147.07 77.6M624 636.51c3.39-5.66 7.43-11.32 13.34-14.26s14.12-2.2 17.84 3.24c11.26 16.46-8.77 55.37-14.74 71.67-9.56 26.11-19.87 52.65-20.54 80.82.71-29.86 19.52-66.19 33.62-91.73 10.44-18.92 20.82-59.58 46.18-63a9.52 9.52 0 015.83.71c2.18 1.18 3.39 3.6 4.11 6 3.48 11.37-1.37 23.5-6.69 34.14a330 330 0 01-35.66 56.25c-7.86 10-16.32 19.49-24.17 29.48-6.71 8.55-13.2 24.19-22 29.81-15.85-12.4-17.95-51.85-17.59-70.53a146.94 146.94 0 0120.47-72.6zM687.22 768.51q-3.51 33.12-8.82 66M752.19 774C750 798.47 747 816.65 743 841.9M819.55 776.94l-11.17 69.46M543.76 193.25c-16.74-26-14.92-61.2 3.91-85.5M188.56 531.29c5.34-7.94 10.76-13.72 16.11-21.66 4 8.23 8 14.3 12 22.53M414.14 107.73c12.62 9.43 31.53 9.3 44-.31M684 90.09a29.06 29.06 0 01-9.32 16.38c5.22-1.33 10.82 1.58 14.07 5.87s4.61 9.71 5.54 15M586.52 306.94c4 5.39 7.41 11.2 11 16.86A151.07 151.07 0 01613 303.61"/><path d="M53.3 835c-13.3 16.33-23.58 37.59-18.71 58.13 4.61 19.43 21.88 33.65 40.41 41.08 47.48 19 103.78 5.29 150.26-10.35 16.09-5.41 31.81-11.82 47.53-18.23l66.62-27.14c6.47-2.64 13-5.28 19.29-8.26" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" fill="none"/><path class="cls-1" d="M379.38 903.5c-23.23-18.18-22-37.3-21.61-64.12 1.49-98.06-2-232-7.46-330.88"/><path class="cls-1" d="M357.16 848C278 895 184.61 923.66 92.77 916.75c-12.8-1-25.72-2.64-37.64-7.4S31.31 897.22 29 883.76a106.53 106.53 0 011.2-42M921.85 293.15q64.59-35.92 130.43-69.51M920.4 310.67q65.19-39.76 132.19-76.47M224.11 304.24c45.67-14 87.46-32.88 133.72-49M222.41 329q66.59-26.37 131.93-55.53M358.28 748.35c11.6-1.2 23.1 3.36 33 9.48s18.8 13.83 28.73 19.95A107.77 107.77 0 00487.88 793c23.46-2.62 47.32-11.35 64.2-27.86M42.12 902.38a131.85 131.85 0 012.25 26.68M59.05 912l2.11 21.84M84 916.8l1 26.64M118.36 919.47a204.08 204.08 0 002 34.67M150.2 918.39q1.61 16.74 1.8 33.61M180.7 913.6l.82 28.78M207 909.35a153.66 153.66 0 001.11 24M233 903l1.88 19.15M256.47 895.51a137.76 137.76 0 01.71 23.94M274.39 889.61a107.33 107.33 0 01.5 22.38M290.48 883.26l1.12 18.64M303.56 876.88q.45 9.06 1.65 18.06M319.61 870.45a132.37 132.37 0 00.46 19.71M330.13 864.07a111.86 111.86 0 002.46 22.93M340.39 858.71a165.22 165.22 0 011.89 21.88M350.65 853.43l1.84 29.28M367.76 861.41q.48 15.23 2.24 30.36M381.06 871l2.54 39.43M401.3 880.59l1 40.49M436 888.06a252.41 252.41 0 002.47 45.3M470.37 891.25a103.72 103.72 0 002.21 28M505.58 892a188.61 188.61 0 011.33 27.2M548.19 892q.94 15.84 1.49 31.7M589.13 892q1 16.1.58 32.23M632.38 891.51q-.2 16.9.37 33.83M671.44 890.44q.45 16.78 1.48 33.56M713.11 890.45q.51 16.77-.57 33.55M993.35 862.67v30.17M978.08 873.06a237 237 0 01-3 29.35M959.13 880.85A167.46 167.46 0 01957 907M932.58 885.45q-.66 13.57-.53 27.17M893.44 887.34A266.55 266.55 0 01894 932M838.46 888.84l2 43.94M796.48 889.36c.84 14.65 2 30.38 2.88 45M755.08 889.36c.69 12.79 1.1 26.65 1.79 39.44"/></svg>


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "monorepo",
  3 |   "private": true,
  4 |   "description": "A minimalistic routing for React and Preact. Monorepo package.",
  5 |   "type": "module",
  6 |   "workspaces": [
  7 |     "packages/wouter",
  8 |     "packages/wouter-preact"
  9 |   ],
 10 |   "scripts": {
 11 |     "fix:p": "prettier --write \"./**/*.(js|ts){x,}\"",
 12 |     "test": "vitest",
 13 |     "size": "size-limit",
 14 |     "build": "npm run build -ws",
 15 |     "watch": "concurrently -n wouter,wouter-preact \"npm run -w packages/wouter watch\" \"npm run -w packages/wouter-preact watch\"",
 16 |     "lint": "eslint packages/**/*.js",
 17 |     "lint-types": "vitest --typecheck"
 18 |   },
 19 |   "author": "Alexey Taktarov <molefrog@gmail.com>",
 20 |   "repository": {
 21 |     "type": "git",
 22 |     "url": "git+https://github.com/molefrog/wouter.git"
 23 |   },
 24 |   "license": "ISC",
 25 |   "prettier": {
 26 |     "tabWidth": 2,
 27 |     "semi": true,
 28 |     "singleQuote": false,
 29 |     "printWidth": 80
 30 |   },
 31 |   "size-limit": [
 32 |     {
 33 |       "path": "packages/wouter/esm/index.js",
 34 |       "limit": "2500 B",
 35 |       "ignore": [
 36 |         "react",
 37 |         "use-sync-external-store"
 38 |       ]
 39 |     },
 40 |     {
 41 |       "path": "packages/wouter/esm/use-browser-location.js",
 42 |       "limit": "1000 B",
 43 |       "import": "{ useBrowserLocation }",
 44 |       "ignore": [
 45 |         "react",
 46 |         "use-sync-external-store"
 47 |       ]
 48 |     },
 49 |     {
 50 |       "path": "packages/wouter/esm/memory-location.js",
 51 |       "limit": "1000 B",
 52 |       "ignore": [
 53 |         "react",
 54 |         "use-sync-external-store"
 55 |       ]
 56 |     },
 57 |     {
 58 |       "path": "packages/wouter/esm/use-hash-location.js",
 59 |       "limit": "1000 B",
 60 |       "ignore": [
 61 |         "react",
 62 |         "use-sync-external-store"
 63 |       ]
 64 |     },
 65 |     {
 66 |       "path": "packages/wouter-preact/esm/index.js",
 67 |       "limit": "2500 B",
 68 |       "ignore": [
 69 |         "preact",
 70 |         "preact/hooks"
 71 |       ]
 72 |     },
 73 |     {
 74 |       "path": "packages/wouter-preact/esm/use-browser-location.js",
 75 |       "limit": "1000 B",
 76 |       "import": "{ useBrowserLocation }",
 77 |       "ignore": [
 78 |         "preact",
 79 |         "preact/hooks"
 80 |       ]
 81 |     },
 82 |     {
 83 |       "path": "packages/wouter-preact/esm/use-hash-location.js",
 84 |       "limit": "1000 B",
 85 |       "ignore": [
 86 |         "preact",
 87 |         "preact/hooks"
 88 |       ]
 89 |     },
 90 |     {
 91 |       "path": "packages/wouter-preact/esm/memory-location.js",
 92 |       "limit": "1000 B",
 93 |       "ignore": [
 94 |         "preact",
 95 |         "preact/hooks"
 96 |       ]
 97 |     }
 98 |   ],
 99 |   "husky": {
100 |     "hooks": {
101 |       "commit-msg": "npm run fix:p"
102 |     }
103 |   },
104 |   "eslintConfig": {
105 |     "extends": "eslint:recommended",
106 |     "parserOptions": {
107 |       "sourceType": "module",
108 |       "ecmaFeatures": {
109 |         "jsx": true
110 |       }
111 |     },
112 |     "env": {
113 |       "es2020": true,
114 |       "browser": true,
115 |       "node": true
116 |     },
117 |     "rules": {
118 |       "no-unused-vars": [
119 |         "error",
120 |         {
121 |           "varsIgnorePattern": "^_",
122 |           "argsIgnorePattern": "^_"
123 |         }
124 |       ],
125 |       "react-hooks/rules-of-hooks": "error",
126 |       "react-hooks/exhaustive-deps": "warn"
127 |     },
128 |     "plugins": [
129 |       "react-hooks"
130 |     ],
131 |     "ignorePatterns": [
132 |       "types/**"
133 |     ]
134 |   },
135 |   "devDependencies": {
136 |     "@preact/preset-vite": "^2.9.0",
137 |     "@rollup/plugin-alias": "^5.0.0",
138 |     "@rollup/plugin-node-resolve": "^15.0.2",
139 |     "@rollup/plugin-replace": "^5.0.7",
140 |     "@size-limit/preset-small-lib": "^11.2.0",
141 |     "@testing-library/dom": "^10.4.0",
142 |     "@testing-library/jest-dom": "^6.1.4",
143 |     "@testing-library/react": "^16.3.0",
144 |     "@types/babel__core": "^7.20.5",
145 |     "@types/bun": "^1.2.14",
146 |     "@types/react": "^18.2.0",
147 |     "@types/react-test-renderer": "^18.0.0",
148 |     "@vitejs/plugin-react": "^4.0.4",
149 |     "@vitest/coverage-v8": "^3.0.8",
150 |     "concurrently": "^8.2.2",
151 |     "copyfiles": "^2.4.1",
152 |     "eslint": "^7.19.0",
153 |     "eslint-plugin-react-hooks": "^4.6.2",
154 |     "happy-dom": "^17.4.7",
155 |     "husky": "^4.3.0",
156 |     "path-to-regexp": "^6.2.1",
157 |     "preact": "^10.23.2",
158 |     "preact-render-to-string": "^6.5.9",
159 |     "prettier": "^2.4.1",
160 |     "react": "^18.2.0",
161 |     "react-dom": "^18.2.0",
162 |     "rimraf": "^3.0.2",
163 |     "rollup": "^3.29.5",
164 |     "size-limit": "^10.0.1",
165 |     "typescript": "5.2.2",
166 |     "vitest": "^3.0.8"
167 |   }
168 | }
169 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "wouter-preact",
 3 |   "version": "3.7.1",
 4 |   "description": "Minimalist-friendly ~1.5KB router for Preact",
 5 |   "type": "module",
 6 |   "keywords": [
 7 |     "react",
 8 |     "preact",
 9 |     "router",
10 |     "tiny",
11 |     "routing",
12 |     "hooks",
13 |     "useLocation"
14 |   ],
15 |   "files": [
16 |     "esm",
17 |     "types/**/*.d.ts",
18 |     "types/*.d.ts"
19 |   ],
20 |   "main": "esm/index.js",
21 |   "exports": {
22 |     ".": {
23 |       "types": "./types/index.d.ts",
24 |       "default": "./esm/index.js"
25 |     },
26 |     "./use-browser-location": {
27 |       "types": "./types/use-browser-location.d.ts",
28 |       "default": "./esm/use-browser-location.js"
29 |     },
30 |     "./use-hash-location": {
31 |       "types": "./types/use-hash-location.d.ts",
32 |       "default": "./esm/use-hash-location.js"
33 |     },
34 |     "./memory-location": {
35 |       "types": "./types/memory-location.d.ts",
36 |       "default": "./esm/memory-location.js"
37 |     }
38 |   },
39 |   "types": "types/index.d.ts",
40 |   "typesVersions": {
41 |     ">=4.1": {
42 |       "types/index.d.ts": [
43 |         "types/index.d.ts"
44 |       ],
45 |       "use-browser-location": [
46 |         "types/use-browser-location.d.ts"
47 |       ],
48 |       "use-hash-location": [
49 |         "types/use-hash-location.d.ts"
50 |       ],
51 |       "memory-location": [
52 |         "types/memory-location.d.ts"
53 |       ]
54 |     }
55 |   },
56 |   "scripts": {
57 |     "build": "rollup -c",
58 |     "watch": "rollup -c -w",
59 |     "prepublishOnly": "npm run build && cp ../../README.md ."
60 |   },
61 |   "author": "Alexey Taktarov <molefrog@gmail.com>",
62 |   "repository": {
63 |     "type": "git",
64 |     "url": "git+https://github.com/molefrog/wouter.git"
65 |   },
66 |   "license": "Unlicense",
67 |   "peerDependencies": {
68 |     "preact": "^10.0.0"
69 |   },
70 |   "dependencies": {
71 |     "mitt": "^3.0.1",
72 |     "regexparam": "^3.0.0"
73 |   },
74 |   "devDependencies": {
75 |     "wouter": "*"
76 |   }
77 | }
78 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/rollup.config.js:
--------------------------------------------------------------------------------
 1 | import alias from "@rollup/plugin-alias";
 2 | import { nodeResolve } from "@rollup/plugin-node-resolve";
 3 | import { defineConfig } from "rollup";
 4 | 
 5 | export default defineConfig([
 6 |   {
 7 |     input: [
 8 |       "wouter",
 9 |       "wouter/use-browser-location",
10 |       "wouter/use-hash-location",
11 |       "wouter/memory-location",
12 |     ],
13 |     external: ["preact", "preact/hooks", "regexparam", "mitt"],
14 | 
15 |     output: {
16 |       dir: "esm",
17 |       format: "esm",
18 |     },
19 |     plugins: [
20 |       nodeResolve(),
21 |       alias({
22 |         entries: {
23 |           "./react-deps.js": "./src/preact-deps.js",
24 |         },
25 |       }),
26 |     ],
27 |   },
28 | ]);
29 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/src/preact-deps.js:
--------------------------------------------------------------------------------
 1 | import { useState, useLayoutEffect, useEffect, useRef } from "preact/hooks";
 2 | export {
 3 |   isValidElement,
 4 |   createContext,
 5 |   cloneElement,
 6 |   createElement,
 7 |   Fragment,
 8 | } from "preact";
 9 | export {
10 |   useMemo,
11 |   useRef,
12 |   useLayoutEffect as useIsomorphicLayoutEffect,
13 |   useLayoutEffect as useInsertionEffect,
14 |   useState,
15 |   useContext,
16 | } from "preact/hooks";
17 | 
18 | // Copied from:
19 | // https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js
20 | const canUseDOM = !!(
21 |   typeof window !== "undefined" &&
22 |   typeof window.document !== "undefined" &&
23 |   typeof window.document.createElement !== "undefined"
24 | );
25 | 
26 | // TODO: switch to `export { useSyncExternalStore } from "preact/compat"` once we update Preact to >= 10.11.3
27 | function is(x, y) {
28 |   return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
29 | }
30 | export function useSyncExternalStore(subscribe, getSnapshot, getSSRSnapshot) {
31 |   if (getSSRSnapshot && !canUseDOM) getSnapshot = getSSRSnapshot;
32 |   const value = getSnapshot();
33 | 
34 |   const [{ _instance }, forceUpdate] = useState({
35 |     _instance: { _value: value, _getSnapshot: getSnapshot },
36 |   });
37 | 
38 |   useLayoutEffect(() => {
39 |     _instance._value = value;
40 |     _instance._getSnapshot = getSnapshot;
41 | 
42 |     if (!is(_instance._value, getSnapshot())) {
43 |       forceUpdate({ _instance });
44 |     }
45 |   }, [subscribe, value, getSnapshot]);
46 | 
47 |   useEffect(() => {
48 |     if (!is(_instance._value, _instance._getSnapshot())) {
49 |       forceUpdate({ _instance });
50 |     }
51 | 
52 |     return subscribe(() => {
53 |       if (!is(_instance._value, _instance._getSnapshot())) {
54 |         forceUpdate({ _instance });
55 |       }
56 |     });
57 |   }, [subscribe]);
58 | 
59 |   return value;
60 | }
61 | 
62 | // provide forwardRef stub for preact
63 | export function forwardRef(component) {
64 |   return component;
65 | }
66 | 
67 | // Userland polyfill while we wait for the forthcoming
68 | // https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
69 | // Note: "A high-fidelty polyfill for useEvent is not possible because
70 | // there is no lifecycle or Hook in React that we can use to switch
71 | // .current at the right timing."
72 | // So we will have to make do with this "close enough" approach for now.
73 | export const useEvent = (fn) => {
74 |   const ref = useRef([fn, (...args) => ref[0](...args)]).current;
75 |   useLayoutEffect(() => {
76 |     ref[0] = fn;
77 |   });
78 |   return ref[1];
79 | };
80 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/test/preact-ssr.test.tsx:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @vitest-environment node
 3 |  */
 4 | 
 5 | import renderToString from "preact-render-to-string";
 6 | import { it, expect, describe } from "vitest";
 7 | import { Router, useLocation } from "wouter-preact";
 8 | 
 9 | describe("Preact SSR", () => {
10 |   it("supports SSR", () => {
11 |     const LocationPrinter = () => <>location = {useLocation()[0]}</>;
12 | 
13 |     const rendered = renderToString(
14 |       <Router ssrPath="/ssr/preact">
15 |         <LocationPrinter />
16 |       </Router>
17 |     );
18 | 
19 |     expect(rendered).toBe("location = /ssr/preact");
20 |   });
21 | });
22 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/test/preact.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, assertType } from "vitest";
 2 | import { useRoute } from "wouter-preact";
 3 | 
 4 | it("should only accept strings", () => {
 5 |   // @ts-expect-error
 6 |   assertType(useRoute(Symbol()));
 7 |   // @ts-expect-error
 8 |   assertType(useRoute());
 9 |   assertType(useRoute("/"));
10 | });
11 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/test/preact.test.tsx:
--------------------------------------------------------------------------------
 1 | import { it, expect, describe, beforeEach, afterEach, vi } from "vitest";
 2 | import { render } from "preact";
 3 | import { act, setupRerender, teardown } from "preact/test-utils";
 4 | 
 5 | import { Route, Link, Switch } from "wouter-preact";
 6 | 
 7 | describe("Preact support", () => {
 8 |   beforeEach(() => {
 9 |     history.replaceState(null, "", "/non-existing/route");
10 |     setupRerender();
11 |   });
12 | 
13 |   afterEach(() => {
14 |     teardown();
15 |   });
16 | 
17 |   it("renders properly and reacts on navigation", () => {
18 |     const container = document.body.appendChild(document.createElement("div"));
19 |     const fn = vi.fn();
20 | 
21 |     const App = () => {
22 |       const handleAsChildClick = vi.fn();
23 | 
24 |       return (
25 |         <>
26 |           <nav>
27 |             <Link href="/albums/all" onClick={fn} data-testid="index-link">
28 |               The Best Albums Ever
29 |             </Link>
30 | 
31 |             <Link
32 |               to="/albums/london-calling"
33 |               asChild
34 |               onClick={handleAsChildClick}
35 |             >
36 |               <a data-testid="featured-link">
37 |                 Featured Now: London Calling, Clash
38 |               </a>
39 |             </Link>
40 |           </nav>
41 | 
42 |           <main data-testid="routes">
43 |             <Switch>
44 |               <>Welcome to the list of {100} greatest albums of all time!</>
45 |               <Route path="/albums/all">Rolling Stones Best 100 Albums</Route>
46 |               <Route path="/albums/:name">
47 |                 {(params) => `Album ${params.name}`}
48 |               </Route>
49 |               <Route path="*">Nothing was found!</Route>
50 |             </Switch>
51 |           </main>
52 |         </>
53 |       );
54 |     };
55 | 
56 |     let node = render(<App />, container);
57 | 
58 |     const routesEl = container.querySelector('[data-testid="routes"]')!;
59 |     const indexLinkEl = container.querySelector('[data-testid="index-link"]')!;
60 |     const featLinkEl = container.querySelector(
61 |       '[data-testid="featured-link"]'
62 |     )!;
63 | 
64 |     // default route should be rendered
65 |     expect(routesEl.textContent).toBe("Nothing was found!");
66 |     expect(featLinkEl.getAttribute("href")).toBe("/albums/london-calling");
67 | 
68 |     // link renders as A element
69 |     expect(indexLinkEl.tagName).toBe("A");
70 | 
71 |     act(() => {
72 |       const evt = new MouseEvent("click", {
73 |         bubbles: true,
74 |         cancelable: true,
75 |         button: 0,
76 |       });
77 | 
78 |       indexLinkEl.dispatchEvent(evt);
79 |     });
80 | 
81 |     // performs a navigation when the link is clicked
82 |     expect(location.pathname).toBe("/albums/all");
83 | 
84 |     // Link accepts an `onClick` prop, fired after the navigation
85 |     expect(fn).toHaveBeenCalledTimes(1);
86 |   });
87 | });
88 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "moduleResolution": "node",
 5 |     "strict": true,
 6 |     "jsx": "react-jsx",
 7 |     "jsxImportSource": "preact",
 8 |     "types": ["preact"]
 9 |   }
10 | }
11 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/index.d.ts:
--------------------------------------------------------------------------------
  1 | // Minimum TypeScript Version: 4.1
  2 | // tslint:disable:no-unnecessary-generics
  3 | 
  4 | import {
  5 |   JSX,
  6 |   FunctionComponent,
  7 |   ComponentType,
  8 |   ComponentChildren,
  9 | } from "preact";
 10 | 
 11 | import {
 12 |   Path,
 13 |   PathPattern,
 14 |   BaseLocationHook,
 15 |   HookReturnValue,
 16 |   HookNavigationOptions,
 17 |   BaseSearchHook,
 18 | } from "./location-hook.js";
 19 | import {
 20 |   BrowserLocationHook,
 21 |   BrowserSearchHook,
 22 | } from "./use-browser-location.js";
 23 | 
 24 | import { RouterObject, RouterOptions, Parser } from "./router.js";
 25 | 
 26 | // these files only export types, so we can re-export them as-is
 27 | // in TS 5.0 we'll be able to use `export type * from ...`
 28 | export * from "./location-hook.js";
 29 | export * from "./router.js";
 30 | 
 31 | import { RouteParams } from "regexparam";
 32 | 
 33 | export type StringRouteParams<T extends string> = RouteParams<T> & {
 34 |   [param: number]: string | undefined;
 35 | };
 36 | export type RegexRouteParams = { [key: string | number]: string | undefined };
 37 | 
 38 | /**
 39 |  * Route patterns and parameters
 40 |  */
 41 | export interface DefaultParams {
 42 |   readonly [paramName: string | number]: string | undefined;
 43 | }
 44 | 
 45 | export type Params<T extends DefaultParams = DefaultParams> = T;
 46 | 
 47 | export type MatchWithParams<T extends DefaultParams = DefaultParams> = [
 48 |   true,
 49 |   Params<T>
 50 | ];
 51 | export type NoMatch = [false, null];
 52 | export type Match<T extends DefaultParams = DefaultParams> =
 53 |   | MatchWithParams<T>
 54 |   | NoMatch;
 55 | 
 56 | /*
 57 |  * Components: <Route />
 58 |  */
 59 | 
 60 | export interface RouteComponentProps<T extends DefaultParams = DefaultParams> {
 61 |   params: T;
 62 | }
 63 | 
 64 | export interface RouteProps<
 65 |   T extends DefaultParams | undefined = undefined,
 66 |   RoutePath extends PathPattern = PathPattern
 67 | > {
 68 |   children?:
 69 |     | ((
 70 |         params: T extends DefaultParams
 71 |           ? T
 72 |           : RoutePath extends string
 73 |           ? StringRouteParams<RoutePath>
 74 |           : RegexRouteParams
 75 |       ) => ComponentChildren)
 76 |     | ComponentChildren;
 77 |   path?: RoutePath;
 78 |   component?: ComponentType<
 79 |     RouteComponentProps<
 80 |       T extends DefaultParams
 81 |         ? T
 82 |         : RoutePath extends string
 83 |         ? StringRouteParams<RoutePath>
 84 |         : RegexRouteParams
 85 |     >
 86 |   >;
 87 |   nest?: boolean;
 88 | }
 89 | 
 90 | export function Route<
 91 |   T extends DefaultParams | undefined = undefined,
 92 |   RoutePath extends PathPattern = PathPattern
 93 | >(props: RouteProps<T, RoutePath>): ReturnType<FunctionComponent>;
 94 | 
 95 | /*
 96 |  * Components: <Link /> & <Redirect />
 97 |  */
 98 | 
 99 | export type NavigationalProps<
100 |   H extends BaseLocationHook = BrowserLocationHook
101 | > = ({ to: Path; href?: never } | { href: Path; to?: never }) &
102 |   HookNavigationOptions<H>;
103 | 
104 | type AsChildProps<ComponentProps, DefaultElementProps> =
105 |   | ({ asChild?: false } & DefaultElementProps)
106 |   | ({ asChild: true } & ComponentProps);
107 | 
108 | type HTMLLinkAttributes = Omit<JSX.HTMLAttributes, "className"> & {
109 |   className?: string | undefined | ((isActive: boolean) => string | undefined);
110 | };
111 | 
112 | export type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
113 |   NavigationalProps<H> &
114 |     AsChildProps<
115 |       { children: ComponentChildren; onClick?: JSX.MouseEventHandler<Element> },
116 |       HTMLLinkAttributes
117 |     >;
118 | 
119 | export type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
120 |   NavigationalProps<H> & {
121 |     children?: never;
122 |   };
123 | 
124 | export function Redirect<H extends BaseLocationHook = BrowserLocationHook>(
125 |   props: RedirectProps<H>,
126 |   context?: any
127 | ): null;
128 | 
129 | export function Link<H extends BaseLocationHook = BrowserLocationHook>(
130 |   props: LinkProps<H>,
131 |   context?: any
132 | ): ReturnType<FunctionComponent>;
133 | 
134 | /*
135 |  * Components: <Switch />
136 |  */
137 | 
138 | export interface SwitchProps {
139 |   location?: string;
140 |   children: ComponentChildren;
141 | }
142 | export const Switch: FunctionComponent<SwitchProps>;
143 | 
144 | /*
145 |  * Components: <Router />
146 |  */
147 | 
148 | export type RouterProps = RouterOptions & {
149 |   children: ComponentChildren;
150 | };
151 | 
152 | export const Router: FunctionComponent<RouterProps>;
153 | 
154 | /*
155 |  * Hooks
156 |  */
157 | 
158 | export function useRouter(): RouterObject;
159 | 
160 | export function useRoute<
161 |   T extends DefaultParams | undefined = undefined,
162 |   RoutePath extends PathPattern = PathPattern
163 | >(
164 |   pattern: RoutePath
165 | ): Match<
166 |   T extends DefaultParams
167 |     ? T
168 |     : RoutePath extends string
169 |     ? StringRouteParams<RoutePath>
170 |     : RegexRouteParams
171 | >;
172 | 
173 | export function useLocation<
174 |   H extends BaseLocationHook = BrowserLocationHook
175 | >(): HookReturnValue<H>;
176 | 
177 | export function useSearch<
178 |   H extends BaseSearchHook = BrowserSearchHook
179 | >(): ReturnType<H>;
180 | 
181 | export type URLSearchParamsInit = ConstructorParameters<
182 |   typeof URLSearchParams
183 | >[0];
184 | 
185 | export type SetSearchParams = (
186 |   nextInit:
187 |     | URLSearchParamsInit
188 |     | ((prev: URLSearchParams) => URLSearchParamsInit),
189 |   options?: { replace?: boolean; state?: any }
190 | ) => void;
191 | 
192 | export function useSearchParams(): [URLSearchParams, SetSearchParams];
193 | 
194 | export function useParams<T = undefined>(): T extends string
195 |   ? StringRouteParams<T>
196 |   : T extends undefined
197 |   ? DefaultParams
198 |   : T;
199 | 
200 | /*
201 |  * Helpers
202 |  */
203 | 
204 | export function matchRoute<
205 |   T extends DefaultParams | undefined = undefined,
206 |   RoutePath extends PathPattern = PathPattern
207 | >(
208 |   parser: Parser,
209 |   pattern: RoutePath,
210 |   path: string,
211 |   loose?: boolean
212 | ): Match<
213 |   T extends DefaultParams
214 |     ? T
215 |     : RoutePath extends string
216 |     ? StringRouteParams<RoutePath>
217 |     : RegexRouteParams
218 | >;
219 | 
220 | // tslint:enable:no-unnecessary-generics
221 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/location-hook.d.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Foundation: useLocation and paths
 3 |  */
 4 | 
 5 | export type Path = string;
 6 | 
 7 | export type PathPattern = string | RegExp;
 8 | 
 9 | export type SearchString = string;
10 | 
11 | // the base useLocation hook type. Any custom hook (including the
12 | // default one) should inherit from it.
13 | export type BaseLocationHook = (
14 |   ...args: any[]
15 | ) => [Path, (path: Path, ...args: any[]) => any];
16 | 
17 | export type BaseSearchHook = (...args: any[]) => SearchString;
18 | 
19 | /*
20 |  * Utility types that operate on hook
21 |  */
22 | 
23 | // Returns the type of the location tuple of the given hook.
24 | export type HookReturnValue<H extends BaseLocationHook> = ReturnType<H>;
25 | 
26 | // Returns the type of the navigation options that hook's push function accepts.
27 | export type HookNavigationOptions<H extends BaseLocationHook> =
28 |   HookReturnValue<H>[1] extends (
29 |     path: Path,
30 |     options: infer R,
31 |     ...rest: any[]
32 |   ) => any
33 |     ? R extends { [k: string]: any }
34 |       ? R
35 |       : {}
36 |     : {};
37 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/memory-location.d.ts:
--------------------------------------------------------------------------------
 1 | import { BaseLocationHook, Path } from "./location-hook.js";
 2 | 
 3 | type Navigate<S = any> = (
 4 |   to: Path,
 5 |   options?: { replace?: boolean; state?: S }
 6 | ) => void;
 7 | 
 8 | type HookReturnValue = { hook: BaseLocationHook; navigate: Navigate };
 9 | type StubHistory = { history: Path[]; reset: () => void };
10 | 
11 | export function memoryLocation(options?: {
12 |   path?: Path;
13 |   static?: boolean;
14 |   record?: false;
15 | }): HookReturnValue;
16 | export function memoryLocation(options?: {
17 |   path?: Path;
18 |   static?: boolean;
19 |   record: true;
20 | }): HookReturnValue & StubHistory;
21 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/router.d.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   Path,
 3 |   SearchString,
 4 |   BaseLocationHook,
 5 |   BaseSearchHook,
 6 | } from "./location-hook.js";
 7 | 
 8 | export type Parser = (
 9 |   route: Path,
10 |   loose?: boolean
11 | ) => { pattern: RegExp; keys: string[] };
12 | 
13 | export type HrefsFormatter = (href: string, router: RouterObject) => string;
14 | 
15 | // the object returned from `useRouter`
16 | export interface RouterObject {
17 |   readonly hook: BaseLocationHook;
18 |   readonly searchHook: BaseSearchHook;
19 |   readonly base: Path;
20 |   readonly ownBase: Path;
21 |   readonly parser: Parser;
22 |   readonly ssrPath?: Path;
23 |   readonly ssrSearch?: SearchString;
24 |   readonly hrefs: HrefsFormatter;
25 | }
26 | 
27 | // basic options to construct a router
28 | export type RouterOptions = {
29 |   hook?: BaseLocationHook;
30 |   searchHook?: BaseSearchHook;
31 |   base?: Path;
32 |   parser?: Parser;
33 |   ssrPath?: Path;
34 |   ssrSearch?: SearchString;
35 |   hrefs?: HrefsFormatter;
36 | };
37 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/use-browser-location.d.ts:
--------------------------------------------------------------------------------
 1 | import { Path, SearchString } from "./location-hook.js";
 2 | 
 3 | type Primitive = string | number | bigint | boolean | null | undefined | symbol;
 4 | export const useLocationProperty: <S extends Primitive>(
 5 |   fn: () => S,
 6 |   ssrFn?: () => S
 7 | ) => S;
 8 | 
 9 | export type BrowserSearchHook = (options?: {
10 |   ssrSearch?: SearchString;
11 | }) => SearchString;
12 | 
13 | export const useSearch: BrowserSearchHook;
14 | 
15 | export const usePathname: (options?: { ssrPath?: Path }) => Path;
16 | 
17 | export const useHistoryState: <T = any>() => T;
18 | 
19 | export const navigate: <S = any>(
20 |   to: string | URL,
21 |   options?: { replace?: boolean; state?: S }
22 | ) => void;
23 | 
24 | /*
25 |  * Default `useLocation`
26 |  */
27 | 
28 | // The type of the default `useLocation` hook that wouter uses.
29 | // It operates on current URL using History API, supports base path and can
30 | // navigate with `pushState` or `replaceState`.
31 | export type BrowserLocationHook = (options?: {
32 |   ssrPath?: Path;
33 | }) => [Path, typeof navigate];
34 | 
35 | export const useBrowserLocation: BrowserLocationHook;
36 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/types/use-hash-location.d.ts:
--------------------------------------------------------------------------------
 1 | import { Path } from "./location-hook.js";
 2 | 
 3 | export function navigate<S = any>(
 4 |   to: Path,
 5 |   options?: { state?: S; replace?: boolean }
 6 | ): void;
 7 | 
 8 | export function useHashLocation(options?: {
 9 |   ssrPath?: Path;
10 | }): [Path, typeof navigate];
11 | 


--------------------------------------------------------------------------------
/packages/wouter-preact/vitest.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineProject } from "vitest/config";
 2 | import preact from "@preact/preset-vite";
 3 | 
 4 | export default defineProject({
 5 |   plugins: [preact()],
 6 |   test: {
 7 |     name: "wouter-preact",
 8 |     environment: "happy-dom",
 9 |   },
10 | });
11 | 


--------------------------------------------------------------------------------
/packages/wouter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "wouter",
 3 |   "version": "3.7.1",
 4 |   "description": "Minimalist-friendly ~1.5KB router for React",
 5 |   "type": "module",
 6 |   "keywords": [
 7 |     "react",
 8 |     "preact",
 9 |     "router",
10 |     "tiny",
11 |     "routing",
12 |     "hooks",
13 |     "useLocation"
14 |   ],
15 |   "files": [
16 |     "esm",
17 |     "types/**/*.d.ts",
18 |     "types/*.d.ts"
19 |   ],
20 |   "main": "esm/index.js",
21 |   "exports": {
22 |     ".": {
23 |       "types": "./types/index.d.ts",
24 |       "default": "./esm/index.js"
25 |     },
26 |     "./use-browser-location": {
27 |       "types": "./types/use-browser-location.d.ts",
28 |       "default": "./esm/use-browser-location.js"
29 |     },
30 |     "./use-hash-location": {
31 |       "types": "./types/use-hash-location.d.ts",
32 |       "default": "./esm/use-hash-location.js"
33 |     },
34 |     "./memory-location": {
35 |       "types": "./types/memory-location.d.ts",
36 |       "default": "./esm/memory-location.js"
37 |     }
38 |   },
39 |   "types": "types/index.d.ts",
40 |   "typesVersions": {
41 |     ">=4.1": {
42 |       "types/index.d.ts": [
43 |         "types/index.d.ts"
44 |       ],
45 |       "use-browser-location": [
46 |         "types/use-browser-location.d.ts"
47 |       ],
48 |       "use-hash-location": [
49 |         "types/use-hash-location.d.ts"
50 |       ],
51 |       "memory-location": [
52 |         "types/memory-location.d.ts"
53 |       ]
54 |     }
55 |   },
56 |   "scripts": {
57 |     "build": "rollup -c",
58 |     "watch": "rollup -c -w",
59 |     "prepublishOnly": "npm run build && cp ../../README.md ."
60 |   },
61 |   "author": "Alexey Taktarov <molefrog@gmail.com>",
62 |   "repository": {
63 |     "type": "git",
64 |     "url": "git+https://github.com/molefrog/wouter.git"
65 |   },
66 |   "license": "Unlicense",
67 |   "peerDependencies": {
68 |     "react": ">=16.8.0"
69 |   },
70 |   "dependencies": {
71 |     "mitt": "^3.0.1",
72 |     "regexparam": "^3.0.0",
73 |     "use-sync-external-store": "^1.0.0"
74 |   }
75 | }
76 | 


--------------------------------------------------------------------------------
/packages/wouter/rollup.config.js:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from "rollup";
 2 | 
 3 | export default defineConfig([
 4 |   {
 5 |     input: ["src/react-deps.js"],
 6 |     output: {
 7 |       dir: "esm",
 8 |       format: "esm",
 9 |     },
10 |     external: [
11 |       "react",
12 |       "use-sync-external-store/shim/index.js",
13 |       "use-sync-external-store/shim/index.native.js",
14 |     ],
15 |   },
16 |   {
17 |     input: [
18 |       "src/index.js",
19 |       "src/use-browser-location.js",
20 |       "src/use-hash-location.js",
21 |       "src/memory-location.js",
22 |     ],
23 |     external: [/react-deps/, "regexparam", "mitt"],
24 |     output: {
25 |       dir: "esm",
26 |       format: "esm",
27 |     },
28 |   },
29 | ]);
30 | 


--------------------------------------------------------------------------------
/packages/wouter/setup-vitest.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/vitest";
2 | 


--------------------------------------------------------------------------------
/packages/wouter/src/index.js:
--------------------------------------------------------------------------------
  1 | import { parse as parsePattern } from "regexparam";
  2 | 
  3 | import {
  4 |   useBrowserLocation,
  5 |   useSearch as useBrowserSearch,
  6 | } from "./use-browser-location.js";
  7 | 
  8 | import {
  9 |   useRef,
 10 |   useContext,
 11 |   createContext,
 12 |   isValidElement,
 13 |   cloneElement,
 14 |   createElement as h,
 15 |   Fragment,
 16 |   forwardRef,
 17 |   useIsomorphicLayoutEffect,
 18 |   useEvent,
 19 |   useMemo,
 20 | } from "./react-deps.js";
 21 | import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";
 22 | 
 23 | /*
 24 |  * Router and router context. Router is a lightweight object that represents the current
 25 |  * routing options: how location is managed, base path etc.
 26 |  *
 27 |  * There is a default router present for most of the use cases, however it can be overridden
 28 |  * via the <Router /> component.
 29 |  */
 30 | 
 31 | const defaultRouter = {
 32 |   hook: useBrowserLocation,
 33 |   searchHook: useBrowserSearch,
 34 |   parser: parsePattern,
 35 |   base: "",
 36 |   // this option is used to override the current location during SSR
 37 |   ssrPath: undefined,
 38 |   ssrSearch: undefined,
 39 |   // optional context to track render state during SSR
 40 |   ssrContext: undefined,
 41 |   // customizes how `href` props are transformed for <Link />
 42 |   hrefs: (x) => x,
 43 | };
 44 | 
 45 | const RouterCtx = createContext(defaultRouter);
 46 | 
 47 | // gets the closest parent router from the context
 48 | export const useRouter = () => useContext(RouterCtx);
 49 | 
 50 | /**
 51 |  * Parameters context. Used by `useParams()` to get the
 52 |  * matched params from the innermost `Route` component.
 53 |  */
 54 | 
 55 | const Params0 = {},
 56 |   ParamsCtx = createContext(Params0);
 57 | 
 58 | export const useParams = () => useContext(ParamsCtx);
 59 | 
 60 | /*
 61 |  * Part 1, Hooks API: useRoute and useLocation
 62 |  */
 63 | 
 64 | // Internal version of useLocation to avoid redundant useRouter calls
 65 | 
 66 | const useLocationFromRouter = (router) => {
 67 |   const [location, navigate] = router.hook(router);
 68 | 
 69 |   // the function reference should stay the same between re-renders, so that
 70 |   // it can be passed down as an element prop without any performance concerns.
 71 |   // (This is achieved via `useEvent`.)
 72 |   return [
 73 |     relativePath(router.base, location),
 74 |     useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)),
 75 |   ];
 76 | };
 77 | 
 78 | export const useLocation = () => useLocationFromRouter(useRouter());
 79 | 
 80 | export const useSearch = () => {
 81 |   const router = useRouter();
 82 |   return sanitizeSearch(router.searchHook(router));
 83 | };
 84 | 
 85 | export const matchRoute = (parser, route, path, loose) => {
 86 |   // if the input is a regexp, skip parsing
 87 |   const { pattern, keys } =
 88 |     route instanceof RegExp
 89 |       ? { keys: false, pattern: route }
 90 |       : parser(route || "*", loose);
 91 | 
 92 |   // array destructuring loses keys, so this is done in two steps
 93 |   const result = pattern.exec(path) || [];
 94 | 
 95 |   // when parser is in "loose" mode, `$base` is equal to the
 96 |   // first part of the route that matches the pattern
 97 |   // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)
 98 |   // we use this for route nesting
 99 |   const [$base, ...matches] = result;
100 | 
101 |   return $base !== undefined
102 |     ? [
103 |         true,
104 | 
105 |         (() => {
106 |           // for regex paths, `keys` will always be false
107 | 
108 |           // an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
109 |           // we "zip" two arrays here to construct the object
110 |           // ["foo"], ["bar"] → { foo: "bar" }
111 |           const groups =
112 |             keys !== false
113 |               ? Object.fromEntries(keys.map((key, i) => [key, matches[i]]))
114 |               : result.groups;
115 | 
116 |           // convert the array to an instance of object
117 |           // this makes it easier to integrate with the existing param implementation
118 |           let obj = { ...matches };
119 | 
120 |           // merge named capture groups with matches array
121 |           groups && Object.assign(obj, groups);
122 | 
123 |           return obj;
124 |         })(),
125 | 
126 |         // the third value if only present when parser is in "loose" mode,
127 |         // so that we can extract the base path for nested routes
128 |         ...(loose ? [$base] : []),
129 |       ]
130 |     : [false, null];
131 | };
132 | 
133 | export const useRoute = (pattern) =>
134 |   matchRoute(useRouter().parser, pattern, useLocation()[0]);
135 | 
136 | /*
137 |  * Part 2, Low Carb Router API: Router, Route, Link, Switch
138 |  */
139 | 
140 | export const Router = ({ children, ...props }) => {
141 |   // the router we will inherit from - it is the closest router in the tree,
142 |   // unless the custom `hook` is provided (in that case it's the default one)
143 |   const parent_ = useRouter();
144 |   const parent = props.hook ? defaultRouter : parent_;
145 | 
146 |   // holds to the context value: the router object
147 |   let value = parent;
148 | 
149 |   // when `ssrPath` contains a `?` character, we can extract the search from it
150 |   const [path, search] = props.ssrPath?.split("?") ?? [];
151 |   if (search) (props.ssrSearch = search), (props.ssrPath = path);
152 | 
153 |   // hooks can define their own `href` formatter (e.g. for hash location)
154 |   props.hrefs = props.hrefs ?? props.hook?.hrefs;
155 | 
156 |   // what is happening below: to avoid unnecessary rerenders in child components,
157 |   // we ensure that the router object reference is stable, unless there are any
158 |   // changes that require reload (e.g. `base` prop changes -> all components that
159 |   // get the router from the context should rerender, even if the component is memoized).
160 |   // the expected behaviour is:
161 |   //
162 |   //   1) when the resulted router is no different from the parent, use parent
163 |   //   2) if the custom `hook` prop is provided, we always inherit from the
164 |   //      default router instead. this resets all previously overridden options.
165 |   //   3) when the router is customized here, it should stay stable between renders
166 |   let ref = useRef({}),
167 |     prev = ref.current,
168 |     next = prev;
169 | 
170 |   for (let k in parent) {
171 |     const option =
172 |       k === "base"
173 |         ? /* base is special case, it is appended to the parent's base */
174 |           parent[k] + (props[k] || "")
175 |         : props[k] || parent[k];
176 | 
177 |     if (prev === next && option !== next[k]) {
178 |       ref.current = next = { ...next };
179 |     }
180 | 
181 |     next[k] = option;
182 | 
183 |     // the new router is no different from the parent or from the memoized value, use parent
184 |     if (option !== parent[k] || option !== value[k]) value = next;
185 |   }
186 | 
187 |   return h(RouterCtx.Provider, { value, children });
188 | };
189 | 
190 | const h_route = ({ children, component }, params) => {
191 |   // React-Router style `component` prop
192 |   if (component) return h(component, { params });
193 | 
194 |   // support render prop or plain children
195 |   return typeof children === "function" ? children(params) : children;
196 | };
197 | 
198 | // Cache params object between renders if values are shallow equal
199 | const useCachedParams = (value) => {
200 |   let prev = useRef(Params0);
201 |   const curr = prev.current;
202 |   return (prev.current =
203 |     // Update cache if number of params changed or any value changed
204 |     Object.keys(value).length !== Object.keys(curr).length ||
205 |     Object.entries(value).some(([k, v]) => v !== curr[k])
206 |       ? value // Return new value if there are changes
207 |       : curr); // Return cached value if nothing changed
208 | };
209 | 
210 | export function useSearchParams() {
211 |   const [location, navigate] = useLocation();
212 | 
213 |   const search = useSearch();
214 |   const searchParams = useMemo(() => new URLSearchParams(search), [search]);
215 | 
216 |   // cached value before next render, so you can call setSearchParams multiple times
217 |   let tempSearchParams = searchParams;
218 | 
219 |   const setSearchParams = useEvent((nextInit, options) => {
220 |     tempSearchParams = new URLSearchParams(
221 |       typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit
222 |     );
223 |     navigate(location + "?" + tempSearchParams, options);
224 |   });
225 | 
226 |   return [searchParams, setSearchParams];
227 | }
228 | 
229 | export const Route = ({ path, nest, match, ...renderProps }) => {
230 |   const router = useRouter();
231 |   const [location] = useLocationFromRouter(router);
232 | 
233 |   const [matches, routeParams, base] =
234 |     // `match` is a special prop to give up control to the parent,
235 |     // it is used by the `Switch` to avoid double matching
236 |     match ?? matchRoute(router.parser, path, location, nest);
237 | 
238 |   // when `routeParams` is `null` (there was no match), the argument
239 |   // below becomes {...null} = {}, see the Object Spread specs
240 |   // https://tc39.es/proposal-object-rest-spread/#AbstractOperations-CopyDataProperties
241 |   const params = useCachedParams({ ...useParams(), ...routeParams });
242 | 
243 |   if (!matches) return null;
244 | 
245 |   const children = base
246 |     ? h(Router, { base }, h_route(renderProps, params))
247 |     : h_route(renderProps, params);
248 | 
249 |   return h(ParamsCtx.Provider, { value: params, children });
250 | };
251 | 
252 | export const Link = forwardRef((props, ref) => {
253 |   const router = useRouter();
254 |   const [currentPath, navigate] = useLocationFromRouter(router);
255 | 
256 |   const {
257 |     to = "",
258 |     href: targetPath = to,
259 |     onClick: _onClick,
260 |     asChild,
261 |     children,
262 |     className: cls,
263 |     /* eslint-disable no-unused-vars */
264 |     replace /* ignore nav props */,
265 |     state /* ignore nav props */,
266 |     /* eslint-enable no-unused-vars */
267 | 
268 |     ...restProps
269 |   } = props;
270 | 
271 |   const onClick = useEvent((event) => {
272 |     // ignores the navigation when clicked using right mouse button or
273 |     // by holding a special modifier key: ctrl, command, win, alt, shift
274 |     if (
275 |       event.ctrlKey ||
276 |       event.metaKey ||
277 |       event.altKey ||
278 |       event.shiftKey ||
279 |       event.button !== 0
280 |     )
281 |       return;
282 | 
283 |     _onClick?.(event);
284 |     if (!event.defaultPrevented) {
285 |       event.preventDefault();
286 |       navigate(targetPath, props);
287 |     }
288 |   });
289 | 
290 |   // handle nested routers and absolute paths
291 |   const href = router.hrefs(
292 |     targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath,
293 |     router // pass router as a second argument for convinience
294 |   );
295 | 
296 |   return asChild && isValidElement(children)
297 |     ? cloneElement(children, { onClick, href })
298 |     : h("a", {
299 |         ...restProps,
300 |         onClick,
301 |         href,
302 |         // `className` can be a function to apply the class if this link is active
303 |         className: cls?.call ? cls(currentPath === targetPath) : cls,
304 |         children,
305 |         ref,
306 |       });
307 | });
308 | 
309 | const flattenChildren = (children) =>
310 |   Array.isArray(children)
311 |     ? children.flatMap((c) =>
312 |         flattenChildren(c && c.type === Fragment ? c.props.children : c)
313 |       )
314 |     : [children];
315 | 
316 | export const Switch = ({ children, location }) => {
317 |   const router = useRouter();
318 |   const [originalLocation] = useLocationFromRouter(router);
319 | 
320 |   for (const element of flattenChildren(children)) {
321 |     let match = 0;
322 | 
323 |     if (
324 |       isValidElement(element) &&
325 |       // we don't require an element to be of type Route,
326 |       // but we do require it to contain a truthy `path` prop.
327 |       // this allows to use different components that wrap Route
328 |       // inside of a switch, for example <AnimatedRoute />.
329 |       (match = matchRoute(
330 |         router.parser,
331 |         element.props.path,
332 |         location || originalLocation,
333 |         element.props.nest
334 |       ))[0]
335 |     )
336 |       return cloneElement(element, { match });
337 |   }
338 | 
339 |   return null;
340 | };
341 | 
342 | export const Redirect = (props) => {
343 |   const { to, href = to } = props;
344 |   const router = useRouter();
345 |   const [, navigate] = useLocationFromRouter(router);
346 |   const redirect = useEvent(() => navigate(to || href, props));
347 |   const { ssrContext } = router;
348 | 
349 |   // redirect is guaranteed to be stable since it is returned from useEvent
350 |   useIsomorphicLayoutEffect(() => {
351 |     redirect();
352 |   }, []); // eslint-disable-line react-hooks/exhaustive-deps
353 | 
354 |   if (ssrContext) {
355 |     ssrContext.redirectTo = to;
356 |   }
357 | 
358 |   return null;
359 | };
360 | 


--------------------------------------------------------------------------------
/packages/wouter/src/memory-location.js:
--------------------------------------------------------------------------------
 1 | import mitt from "mitt";
 2 | import { useSyncExternalStore } from "./react-deps.js";
 3 | 
 4 | /**
 5 |  * In-memory location that supports navigation
 6 |  */
 7 | 
 8 | export const memoryLocation = ({
 9 |   path = "/",
10 |   searchPath = "",
11 |   static: staticLocation,
12 |   record,
13 | } = {}) => {
14 |   let initialPath = path;
15 |   if (searchPath) {
16 |     // join with & if path contains search query, and ? otherwise
17 |     initialPath += path.split("?")[1] ? "&" : "?";
18 |     initialPath += searchPath;
19 |   }
20 | 
21 |   let [currentPath, currentSearch = ""] = initialPath.split("?");
22 |   const history = [initialPath];
23 |   const emitter = mitt();
24 | 
25 |   const navigateImplementation = (path, { replace = false } = {}) => {
26 |     if (record) {
27 |       if (replace) {
28 |         history.splice(history.length - 1, 1, path);
29 |       } else {
30 |         history.push(path);
31 |       }
32 |     }
33 | 
34 |     [currentPath, currentSearch = ""] = path.split("?");
35 |     emitter.emit("navigate", path);
36 |   };
37 | 
38 |   const navigate = !staticLocation ? navigateImplementation : () => null;
39 | 
40 |   const subscribe = (cb) => {
41 |     emitter.on("navigate", cb);
42 |     return () => emitter.off("navigate", cb);
43 |   };
44 | 
45 |   const useMemoryLocation = () => [
46 |     useSyncExternalStore(subscribe, () => currentPath),
47 |     navigate,
48 |   ];
49 | 
50 |   const useMemoryQuery = () =>
51 |     useSyncExternalStore(subscribe, () => currentSearch);
52 | 
53 |   function reset() {
54 |     // clean history array with mutation to preserve link
55 |     history.splice(0, history.length);
56 | 
57 |     navigateImplementation(initialPath);
58 |   }
59 | 
60 |   return {
61 |     hook: useMemoryLocation,
62 |     searchHook: useMemoryQuery,
63 |     navigate,
64 |     history: record ? history : undefined,
65 |     reset: record ? reset : undefined,
66 |   };
67 | };
68 | 


--------------------------------------------------------------------------------
/packages/wouter/src/paths.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Transforms `path` into its relative `base` version
 3 |  * If base isn't part of the path provided returns absolute path e.g. `~/app`
 4 |  */
 5 | const _relativePath = (base, path) =>
 6 |   !path.toLowerCase().indexOf(base.toLowerCase())
 7 |     ? path.slice(base.length) || "/"
 8 |     : "~" + path;
 9 | 
10 | /**
11 |  * When basepath is `undefined` or '/' it is ignored (we assume it's empty string)
12 |  */
13 | const baseDefaults = (base = "") => (base === "/" ? "" : base);
14 | 
15 | export const absolutePath = (to, base) =>
16 |   to[0] === "~" ? to.slice(1) : baseDefaults(base) + to;
17 | 
18 | export const relativePath = (base = "", path) =>
19 |   _relativePath(unescape(baseDefaults(base)), unescape(path));
20 | 
21 | /*
22 |  * Removes leading question mark
23 |  */
24 | const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str);
25 | 
26 | /*
27 |  * decodes escape sequences such as %20
28 |  */
29 | const unescape = (str) => {
30 |   try {
31 |     return decodeURI(str);
32 |   } catch (_e) {
33 |     // fail-safe mode: if string can't be decoded do nothing
34 |     return str;
35 |   }
36 | };
37 | 
38 | export const sanitizeSearch = (search) => unescape(stripQm(search));
39 | 


--------------------------------------------------------------------------------
/packages/wouter/src/react-deps.js:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | // React.useInsertionEffect is not available in React <18
 4 | // This hack fixes a transpilation issue on some apps
 5 | const useBuiltinInsertionEffect = React["useInsertion" + "Effect"];
 6 | 
 7 | export {
 8 |   useMemo,
 9 |   useRef,
10 |   useState,
11 |   useContext,
12 |   createContext,
13 |   isValidElement,
14 |   cloneElement,
15 |   createElement,
16 |   Fragment,
17 |   forwardRef,
18 | } from "react";
19 | 
20 | // To resolve webpack 5 errors, while not presenting problems for native,
21 | // we copy the approaches from https://github.com/TanStack/query/pull/3561
22 | // and https://github.com/TanStack/query/pull/3601
23 | // ~ Show this aging PR some love to remove the need for this hack:
24 | //   https://github.com/facebook/react/pull/25231 ~
25 | export { useSyncExternalStore } from "./use-sync-external-store.js";
26 | 
27 | // Copied from:
28 | // https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js
29 | const canUseDOM = !!(
30 |   typeof window !== "undefined" &&
31 |   typeof window.document !== "undefined" &&
32 |   typeof window.document.createElement !== "undefined"
33 | );
34 | 
35 | // Copied from:
36 | // https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts
37 | // "React currently throws a warning when using useLayoutEffect on the server.
38 | // To get around it, we can conditionally useEffect on the server (no-op) and
39 | // useLayoutEffect in the browser."
40 | export const useIsomorphicLayoutEffect = canUseDOM
41 |   ? React.useLayoutEffect
42 |   : React.useEffect;
43 | 
44 | // useInsertionEffect is already a noop on the server.
45 | // See: https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js
46 | export const useInsertionEffect =
47 |   useBuiltinInsertionEffect || useIsomorphicLayoutEffect;
48 | 
49 | // Userland polyfill while we wait for the forthcoming
50 | // https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
51 | // Note: "A high-fidelity polyfill for useEvent is not possible because
52 | // there is no lifecycle or Hook in React that we can use to switch
53 | // .current at the right timing."
54 | // So we will have to make do with this "close enough" approach for now.
55 | export const useEvent = (fn) => {
56 |   const ref = React.useRef([fn, (...args) => ref[0](...args)]).current;
57 |   // Per Dan Abramov: useInsertionEffect executes marginally closer to the
58 |   // correct timing for ref synchronization than useLayoutEffect on React 18.
59 |   // See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
60 |   useInsertionEffect(() => {
61 |     ref[0] = fn;
62 |   });
63 |   return ref[1];
64 | };
65 | 


--------------------------------------------------------------------------------
/packages/wouter/src/use-browser-location.js:
--------------------------------------------------------------------------------
 1 | import { useSyncExternalStore } from "./react-deps.js";
 2 | 
 3 | /**
 4 |  * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
 5 |  */
 6 | const eventPopstate = "popstate";
 7 | const eventPushState = "pushState";
 8 | const eventReplaceState = "replaceState";
 9 | const eventHashchange = "hashchange";
10 | const events = [
11 |   eventPopstate,
12 |   eventPushState,
13 |   eventReplaceState,
14 |   eventHashchange,
15 | ];
16 | 
17 | const subscribeToLocationUpdates = (callback) => {
18 |   for (const event of events) {
19 |     addEventListener(event, callback);
20 |   }
21 |   return () => {
22 |     for (const event of events) {
23 |       removeEventListener(event, callback);
24 |     }
25 |   };
26 | };
27 | 
28 | export const useLocationProperty = (fn, ssrFn) =>
29 |   useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn);
30 | 
31 | const currentSearch = () => location.search;
32 | 
33 | export const useSearch = ({ ssrSearch = "" } = {}) =>
34 |   useLocationProperty(currentSearch, () => ssrSearch);
35 | 
36 | const currentPathname = () => location.pathname;
37 | 
38 | export const usePathname = ({ ssrPath } = {}) =>
39 |   useLocationProperty(
40 |     currentPathname,
41 |     ssrPath ? () => ssrPath : currentPathname
42 |   );
43 | 
44 | const currentHistoryState = () => history.state;
45 | export const useHistoryState = () =>
46 |   useLocationProperty(currentHistoryState, () => null);
47 | 
48 | export const navigate = (to, { replace = false, state = null } = {}) =>
49 |   history[replace ? eventReplaceState : eventPushState](state, "", to);
50 | 
51 | // the 2nd argument of the `useBrowserLocation` return value is a function
52 | // that allows to perform a navigation.
53 | export const useBrowserLocation = (opts = {}) => [usePathname(opts), navigate];
54 | 
55 | const patchKey = Symbol.for("wouter_v3");
56 | 
57 | // While History API does have `popstate` event, the only
58 | // proper way to listen to changes via `push/replaceState`
59 | // is to monkey-patch these methods.
60 | //
61 | // See https://stackoverflow.com/a/4585031
62 | if (typeof history !== "undefined" && typeof window[patchKey] === "undefined") {
63 |   for (const type of [eventPushState, eventReplaceState]) {
64 |     const original = history[type];
65 |     // TODO: we should be using unstable_batchedUpdates to avoid multiple re-renders,
66 |     // however that will require an additional peer dependency on react-dom.
67 |     // See: https://github.com/reactwg/react-18/discussions/86#discussioncomment-1567149
68 |     history[type] = function () {
69 |       const result = original.apply(this, arguments);
70 |       const event = new Event(type);
71 |       event.arguments = arguments;
72 | 
73 |       dispatchEvent(event);
74 |       return result;
75 |     };
76 |   }
77 | 
78 |   // patch history object only once
79 |   // See: https://github.com/molefrog/wouter/issues/167
80 |   Object.defineProperty(window, patchKey, { value: true });
81 | }
82 | 


--------------------------------------------------------------------------------
/packages/wouter/src/use-hash-location.js:
--------------------------------------------------------------------------------
 1 | import { useSyncExternalStore } from "./react-deps.js";
 2 | 
 3 | // array of callback subscribed to hash updates
 4 | const listeners = {
 5 |   v: [],
 6 | };
 7 | 
 8 | const onHashChange = () => listeners.v.forEach((cb) => cb());
 9 | 
10 | // we subscribe to `hashchange` only once when needed to guarantee that
11 | // all listeners are called synchronously
12 | const subscribeToHashUpdates = (callback) => {
13 |   if (listeners.v.push(callback) === 1)
14 |     addEventListener("hashchange", onHashChange);
15 | 
16 |   return () => {
17 |     listeners.v = listeners.v.filter((i) => i !== callback);
18 |     if (!listeners.v.length) removeEventListener("hashchange", onHashChange);
19 |   };
20 | };
21 | 
22 | // leading '#' is ignored, leading '/' is optional
23 | const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, "");
24 | 
25 | export const navigate = (to, { state = null, replace = false } = {}) => {
26 |   const [hash, search] = to.replace(/^#?\/?/, "").split("?");
27 | 
28 |   const newRelativePath =
29 |     location.pathname + (search ? `?${search}` : location.search) + `#/${hash}`;
30 |   const oldURL = location.href;
31 |   const newURL = new URL(newRelativePath, location.origin).href;
32 | 
33 |   if (replace) {
34 |     history.replaceState(state, "", newRelativePath);
35 |   } else {
36 |     history.pushState(state, "", newRelativePath);
37 |   }
38 | 
39 |   const event =
40 |     typeof HashChangeEvent !== "undefined"
41 |       ? new HashChangeEvent("hashchange", { oldURL, newURL })
42 |       : new Event("hashchange", { detail: { oldURL, newURL } });
43 | 
44 |   dispatchEvent(event);
45 | };
46 | 
47 | export const useHashLocation = ({ ssrPath = "/" } = {}) => [
48 |   useSyncExternalStore(
49 |     subscribeToHashUpdates,
50 |     currentHashLocation,
51 |     () => ssrPath
52 |   ),
53 |   navigate,
54 | ];
55 | 
56 | useHashLocation.hrefs = (href) => "#" + href;
57 | 


--------------------------------------------------------------------------------
/packages/wouter/src/use-sync-external-store.js:
--------------------------------------------------------------------------------
1 | export { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
2 | 


--------------------------------------------------------------------------------
/packages/wouter/src/use-sync-external-store.native.js:
--------------------------------------------------------------------------------
1 | export { useSyncExternalStore } from "use-sync-external-store/shim/index.native.js";
2 | 


--------------------------------------------------------------------------------
/packages/wouter/test/global-this-at-component-level.test.tsx:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @vitest-environment node
 3 |  */
 4 | 
 5 | import { test, expect, describe } from "vitest";
 6 | import { renderToStaticMarkup } from "react-dom/server";
 7 | import { useSearch, useLocation, Router } from "wouter";
 8 | 
 9 | describe("useSearch", () => {
10 |   test("works in node", () => {
11 |     const App = () => {
12 |       const search = useSearch();
13 |       return <>{search}</>;
14 |     };
15 | 
16 |     const rendered = renderToStaticMarkup(
17 |       <Router ssrSearch="?foo=1">
18 |         <App />
19 |       </Router>
20 |     );
21 |     expect(rendered).toBe("foo=1");
22 |   });
23 | 
24 |   test("works in node without options", () => {
25 |     const App = () => {
26 |       const search = useSearch();
27 |       return <>search: {search}</>;
28 |     };
29 | 
30 |     const rendered = renderToStaticMarkup(<App />);
31 |     expect(rendered).toBe("search: ");
32 |   });
33 | });
34 | 
35 | test("useLocation works in node", () => {
36 |   const App = () => {
37 |     const [path] = useLocation();
38 |     return <>{path}</>;
39 |   };
40 | 
41 |   const rendered = renderToStaticMarkup(
42 |     <Router ssrPath="/hello-from-server">
43 |       <App />
44 |     </Router>
45 |   );
46 |   expect(rendered).toBe("/hello-from-server");
47 | });
48 | 


--------------------------------------------------------------------------------
/packages/wouter/test/global-this-at-top-level.test.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @vitest-environment node
 3 |  */
 4 | 
 5 | import { test, expect } from "vitest";
 6 | 
 7 | test("use-browser-location should work in node environment", () => {
 8 |   expect(() => import("wouter/use-browser-location")).not.toThrow();
 9 | });
10 | 
11 | test("wouter should work in node environment", () => {
12 |   expect(() => import("wouter")).not.toThrow();
13 | });
14 | 


--------------------------------------------------------------------------------
/packages/wouter/test/history-patch.test.ts:
--------------------------------------------------------------------------------
 1 | import { useLocation as reactHook } from "wouter";
 2 | import { useLocation as preactHook } from "wouter-preact";
 3 | import { renderHook, act } from "@testing-library/react";
 4 | 
 5 | import { vi, it, expect, describe } from "vitest";
 6 | 
 7 | describe("history patch", () => {
 8 |   it("exports should exists", () => {
 9 |     expect(reactHook).toBeDefined();
10 |     expect(preactHook).toBeDefined();
11 |   });
12 | 
13 |   it("history should be patched once", () => {
14 |     const fn = vi.fn();
15 |     const { result, unmount } = renderHook(() => reactHook());
16 | 
17 |     addEventListener("pushState", (e) => {
18 |       fn();
19 |     });
20 | 
21 |     expect(result.current[0]).toBe("/");
22 |     expect(fn).toBeCalledTimes(0);
23 | 
24 |     act(() => result.current[1]("/hello"));
25 |     act(() => result.current[1]("/world"));
26 | 
27 |     expect(result.current[0]).toBe("/world");
28 |     expect(fn).toBeCalledTimes(2);
29 | 
30 |     unmount();
31 |   });
32 | });
33 | 


--------------------------------------------------------------------------------
/packages/wouter/test/link.test-d.tsx:
--------------------------------------------------------------------------------
  1 | import { describe, expectTypeOf, it } from "vitest";
  2 | import { Link, LinkProps, type Path } from "wouter";
  3 | import * as React from "react";
  4 | 
  5 | type NetworkLocationHook = () => [
  6 |   Path,
  7 |   (path: string, options: { host: string; retries?: number }) => void
  8 | ];
  9 | 
 10 | describe("<Link /> types", () => {
 11 |   it("should have required prop href", () => {
 12 |     // @ts-expect-error
 13 |     <Link>test</Link>;
 14 |     <Link href="/">test</Link>;
 15 |   });
 16 | 
 17 |   it("does not allow `to` and `href` props to be used at the same time", () => {
 18 |     // @ts-expect-error
 19 |     <Link to="/hello" href="/world">
 20 |       Hello
 21 |     </Link>;
 22 |   });
 23 | 
 24 |   it("should inherit props from `HTMLAnchorElement`", () => {
 25 |     <Link to="/hello" className="hello">
 26 |       Hello
 27 |     </Link>;
 28 | 
 29 |     <Link to="/hello" style={{}}>
 30 |       Hello
 31 |     </Link>;
 32 | 
 33 |     <Link to="/hello" target="_blank">
 34 |       Hello
 35 |     </Link>;
 36 | 
 37 |     <Link to="/hello" download ping="he-he">
 38 |       Hello
 39 |     </Link>;
 40 |   });
 41 | 
 42 |   it("can accept function as `className`", () => {
 43 |     <Link
 44 |       href="/"
 45 |       className={(isActive) => (isActive ? "active" : "non-active")}
 46 |     />;
 47 | 
 48 |     <Link
 49 |       href="/"
 50 |       className={(isActive) => (isActive ? "active" : undefined)}
 51 |     />;
 52 |   });
 53 | 
 54 |   it("should support other navigation params", () => {
 55 |     <Link href="/" state={{ a: "foo" }}>
 56 |       test
 57 |     </Link>;
 58 | 
 59 |     <Link href="/" replace>
 60 |       test
 61 |     </Link>;
 62 | 
 63 |     // @ts-expect-error
 64 |     <Link to="/" replace={{ nope: 1 }}>
 65 |       Hello
 66 |     </Link>;
 67 | 
 68 |     <Link href="/" state={undefined}>
 69 |       test
 70 |     </Link>;
 71 |   });
 72 | 
 73 |   it("should work with generic type", () => {
 74 |     <Link<NetworkLocationHook> href="/" host="wouter.com">
 75 |       test
 76 |     </Link>;
 77 | 
 78 |     // @ts-expect-error
 79 |     <Link<NetworkLocationHook> href="/">test</Link>;
 80 | 
 81 |     <Link<NetworkLocationHook> href="/" host="wouter.com" retries={4}>
 82 |       test
 83 |     </Link>;
 84 |   });
 85 | });
 86 | 
 87 | describe("<Link /> with ref", () => {
 88 |   it("should work", () => {
 89 |     const ref = React.useRef<HTMLAnchorElement>(null);
 90 | 
 91 |     <Link to="/" ref={ref}>
 92 |       Hello
 93 |     </Link>;
 94 |   });
 95 | 
 96 |   it("should have error when type is `unknown`", () => {
 97 |     const ref = React.useRef();
 98 | 
 99 |     // @ts-expect-error
100 |     <Link to="/" ref={ref}>
101 |       Hello
102 |     </Link>;
103 |   });
104 | 
105 |   it("should have error when type is miss matched", () => {
106 |     const ref = React.useRef<HTMLAreaElement>(null);
107 | 
108 |     // @ts-expect-error
109 |     <Link to="/" ref={ref}>
110 |       Hello
111 |     </Link>;
112 |   });
113 | });
114 | 
115 | describe("<Link /> with `asChild` prop", () => {
116 |   it("should work", () => {
117 |     <Link to="/" asChild>
118 |       <a>Hello</a>
119 |     </Link>;
120 |   });
121 | 
122 |   it("does not allow `to` and `href` props to be used at the same time", () => {
123 |     // @ts-expect-error
124 |     <Link to="/hello" href="/world" asChild>
125 |       <a>Hello</a>
126 |     </Link>;
127 |   });
128 | 
129 |   it("can only have valid element as a child", () => {
130 |     // @ts-expect-error strings are not valid children
131 |     <Link to="/" asChild>
132 |       {true ? "Hello" : "World"}
133 |     </Link>;
134 | 
135 |     // @ts-expect-error can't use multiple nodes as children
136 |     <Link to="/" asChild>
137 |       <a>Link</a>
138 |       <div>icon</div>
139 |     </Link>;
140 |   });
141 | 
142 |   it("does not allow other props", () => {
143 |     // @ts-expect-error
144 |     <Link to="/" asChild className="">
145 |       <a>Hello</a>
146 |     </Link>;
147 | 
148 |     // @ts-expect-error
149 |     <Link to="/" asChild style={{}}>
150 |       <a>Hello</a>
151 |     </Link>;
152 | 
153 |     // @ts-expect-error
154 |     <Link to="/" asChild unknown={10}>
155 |       <a>Hello</a>
156 |     </Link>;
157 | 
158 |     // @ts-expect-error
159 |     <Link to="/" asChild ref={null}>
160 |       <a>Hello</a>
161 |     </Link>;
162 |   });
163 | 
164 |   it("should support other navigation params", () => {
165 |     <Link to="/" asChild replace>
166 |       <a>Hello</a>
167 |     </Link>;
168 | 
169 |     // @ts-expect-error
170 |     <Link to="/" asChild replace={12}>
171 |       <a>Hello</a>
172 |     </Link>;
173 | 
174 |     <Link to="/" asChild state={{ hello: "world" }}>
175 |       <a>Hello</a>
176 |     </Link>;
177 |   });
178 | 
179 |   it("should work with generic type", () => {
180 |     <Link<NetworkLocationHook> asChild to="/" host="wouter.com">
181 |       <div>test</div>
182 |     </Link>;
183 | 
184 |     // @ts-expect-error
185 |     <Link<NetworkLocationHook> asChild to="/">
186 |       <div>test</div>
187 |     </Link>;
188 | 
189 |     <Link<NetworkLocationHook> asChild to="/" host="wouter.com" retries={4}>
190 |       <div>test</div>
191 |     </Link>;
192 |   });
193 | 
194 |   it("accepts `onClick` prop that overwrites child's handler", () => {
195 |     <Link
196 |       to="/"
197 |       asChild
198 |       onClick={(e) => {
199 |         expectTypeOf(e).toEqualTypeOf<React.MouseEvent>();
200 |       }}
201 |     >
202 |       <a>Hello</a>
203 |     </Link>;
204 |   });
205 | 
206 |   it("should work with `ComponentProps`", () => {
207 |     type LinkComponentProps = React.ComponentProps<typeof Link>;
208 | 
209 |     // Because Link is a generic component, the props
210 |     // cant't contain navigation options of the default generic
211 |     // parameter `BrowserLocationHook`.
212 |     // So the best we can get are the props such as `href` etc.
213 |     expectTypeOf<LinkComponentProps>().toMatchTypeOf<LinkProps>();
214 |   });
215 | });
216 | 


--------------------------------------------------------------------------------
/packages/wouter/test/link.test.tsx:
--------------------------------------------------------------------------------
  1 | import { type MouseEventHandler } from "react";
  2 | import { it, expect, afterEach, vi, describe } from "vitest";
  3 | import { render, cleanup, fireEvent, act } from "@testing-library/react";
  4 | 
  5 | import { Router, Link } from "wouter";
  6 | import { memoryLocation } from "wouter/memory-location";
  7 | 
  8 | afterEach(cleanup);
  9 | 
 10 | describe("<Link />", () => {
 11 |   it("renders a link with proper attributes", () => {
 12 |     const { getByText } = render(
 13 |       <Link href="/about" className="link--active">
 14 |         Click Me
 15 |       </Link>
 16 |     );
 17 | 
 18 |     const element = getByText("Click Me");
 19 | 
 20 |     expect(element).toBeInTheDocument();
 21 |     expect(element).toHaveAttribute("href", "/about");
 22 |     expect(element).toHaveClass("link--active");
 23 |   });
 24 | 
 25 |   it("passes ref to <a />", () => {
 26 |     const refCallback = vi.fn<[HTMLAnchorElement], void>();
 27 |     const { getByText } = render(
 28 |       <Link href="/" ref={refCallback}>
 29 |         Testing
 30 |       </Link>
 31 |     );
 32 | 
 33 |     const element = getByText("Testing");
 34 | 
 35 |     expect(element).toBeInTheDocument();
 36 |     expect(element).toHaveAttribute("href", "/");
 37 | 
 38 |     expect(refCallback).toBeCalledTimes(1);
 39 |     expect(refCallback).toBeCalledWith(element);
 40 |   });
 41 | 
 42 |   it("still creates a plain link when nothing is passed", () => {
 43 |     const { getByTestId } = render(<Link href="/about" data-testid="link" />);
 44 | 
 45 |     const element = getByTestId("link");
 46 | 
 47 |     expect(element).toBeInTheDocument();
 48 |     expect(element).toHaveAttribute("href", "/about");
 49 |     expect(element).toBeEmptyDOMElement();
 50 |   });
 51 | 
 52 |   it("supports `to` prop as an alias to `href`", () => {
 53 |     const { getByText } = render(<Link to="/about">Hello</Link>);
 54 |     const element = getByText("Hello");
 55 | 
 56 |     expect(element).toBeInTheDocument();
 57 |     expect(element).toHaveAttribute("href", "/about");
 58 |   });
 59 | 
 60 |   it("performs a navigation when the link is clicked", () => {
 61 |     const { getByTestId } = render(
 62 |       <Link href="/goo-baz" data-testid="link">
 63 |         link
 64 |       </Link>
 65 |     );
 66 | 
 67 |     fireEvent.click(getByTestId("link"));
 68 | 
 69 |     expect(location.pathname).toBe("/goo-baz");
 70 |   });
 71 | 
 72 |   it("supports replace navigation", () => {
 73 |     const { getByTestId } = render(
 74 |       <Link href="/goo-baz" replace data-testid="link">
 75 |         link
 76 |       </Link>
 77 |     );
 78 | 
 79 |     const histBefore = history.length;
 80 | 
 81 |     fireEvent.click(getByTestId("link"));
 82 | 
 83 |     expect(location.pathname).toBe("/goo-baz");
 84 |     expect(history.length).toBe(histBefore);
 85 |   });
 86 | 
 87 |   it("ignores the navigation when clicked with modifiers", () => {
 88 |     const { getByTestId } = render(
 89 |       <Link href="/users" data-testid="link">
 90 |         click
 91 |       </Link>
 92 |     );
 93 |     const clickEvt = new MouseEvent("click", {
 94 |       bubbles: true,
 95 |       cancelable: true,
 96 |       button: 0,
 97 |       ctrlKey: true,
 98 |     });
 99 | 
100 |     // js-dom doesn't implement browser navigation (e.g. changing location
101 |     // when a link is clicked) so we need just ingore it to avoid warnings
102 |     clickEvt.preventDefault();
103 | 
104 |     fireEvent(getByTestId("link"), clickEvt);
105 |     expect(location.pathname).not.toBe("/users");
106 |   });
107 | 
108 |   it("ignores the navigation when event is cancelled", () => {
109 |     const clickHandler: MouseEventHandler = (e) => {
110 |       e.preventDefault();
111 |     };
112 | 
113 |     const { getByTestId } = render(
114 |       <Link href="/users" data-testid="link" onClick={clickHandler}>
115 |         click
116 |       </Link>
117 |     );
118 | 
119 |     fireEvent.click(getByTestId("link"));
120 |     expect(location.pathname).not.toBe("/users");
121 |   });
122 | 
123 |   it("accepts an `onClick` prop, fired before the navigation", () => {
124 |     const clickHandler = vi.fn();
125 | 
126 |     const { getByTestId } = render(
127 |       <Link href="/" onClick={clickHandler} data-testid="link" />
128 |     );
129 | 
130 |     fireEvent.click(getByTestId("link"));
131 |     expect(clickHandler).toHaveBeenCalledTimes(1);
132 |   });
133 | 
134 |   it("renders `href` with basepath", () => {
135 |     const { getByTestId } = render(
136 |       <Router base="/app">
137 |         <Link href="/dashboard" data-testid="link" />
138 |       </Router>
139 |     );
140 | 
141 |     const link = getByTestId("link");
142 |     expect(link.getAttribute("href")).toBe("/app/dashboard");
143 |   });
144 | 
145 |   it("renders `href` with absolute links", () => {
146 |     const { getByTestId } = render(
147 |       <Router base="/app">
148 |         <Link href="~/home" data-testid="link" />
149 |       </Router>
150 |     );
151 | 
152 |     const element = getByTestId("link");
153 |     expect(element).toHaveAttribute("href", "/home");
154 |   });
155 | 
156 |   it("supports history state", () => {
157 |     const testState = { hello: "world" };
158 |     const { getByTestId } = render(
159 |       <Link href="/goo-baz" state={testState} data-testid="link">
160 |         link
161 |       </Link>
162 |     );
163 | 
164 |     fireEvent.click(getByTestId("link"));
165 |     expect(location.pathname).toBe("/goo-baz");
166 |     expect(history.state).toStrictEqual(testState);
167 |   });
168 | 
169 |   it("can be configured to use custom href formatting", () => {
170 |     const formatter = (href: string) => `#${href}`;
171 | 
172 |     const { getByTestId } = render(
173 |       <>
174 |         <Router hrefs={formatter}>
175 |           <Link href="/" data-testid="root" />
176 |           <Link href="/home" data-testid="home" />
177 |         </Router>
178 | 
179 |         <Router base="/app" hrefs={formatter}>
180 |           <Link href="~/home" data-testid="absolute" />
181 |         </Router>
182 |       </>
183 |     );
184 | 
185 |     expect(getByTestId("root")).toHaveAttribute("href", "#/");
186 |     expect(getByTestId("home")).toHaveAttribute("href", "#/home");
187 |     expect(getByTestId("absolute")).toHaveAttribute("href", "#/home");
188 |   });
189 | });
190 | 
191 | describe("active links", () => {
192 |   it("proxies `className` when it is a string", () => {
193 |     const { getByText } = render(
194 |       <Link href="/" className="link--active warning">
195 |         Click Me
196 |       </Link>
197 |     );
198 | 
199 |     const element = getByText("Click Me");
200 |     expect(element).toHaveAttribute("class", "link--active warning");
201 |   });
202 | 
203 |   it("calls the `className` function with active link flag", () => {
204 |     const { navigate, hook } = memoryLocation({ path: "/" });
205 | 
206 |     const { getByText } = render(
207 |       <Router hook={hook}>
208 |         <Link
209 |           href="/"
210 |           className={(isActive) => {
211 |             return [isActive ? "active" : "", "link"].join(" ");
212 |           }}
213 |         >
214 |           Click Me
215 |         </Link>
216 |       </Router>
217 |     );
218 | 
219 |     const element = getByText("Click Me");
220 |     expect(element).toBeInTheDocument();
221 |     expect(element).toHaveClass("active");
222 |     expect(element).toHaveClass("link");
223 | 
224 |     act(() => navigate("/about"));
225 | 
226 |     expect(element).not.toHaveClass("active");
227 |     expect(element).toHaveClass("link");
228 |   });
229 | 
230 |   it("correctly highlights active links when using custom href formatting", () => {
231 |     const formatter = (href: string) => `#${href}`;
232 |     const { navigate, hook } = memoryLocation({ path: "/" });
233 | 
234 |     const { getByText } = render(
235 |       <Router hook={hook} hrefs={formatter}>
236 |         <Link
237 |           href="/"
238 |           className={(isActive) => {
239 |             return [isActive ? "active" : "", "link"].join(" ");
240 |           }}
241 |         >
242 |           Click Me
243 |         </Link>
244 |       </Router>
245 |     );
246 | 
247 |     const element = getByText("Click Me");
248 |     expect(element).toBeInTheDocument();
249 |     expect(element).toHaveClass("active");
250 |     expect(element).toHaveClass("link");
251 | 
252 |     act(() => navigate("/about"));
253 | 
254 |     expect(element).not.toHaveClass("active");
255 |     expect(element).toHaveClass("link");
256 |   });
257 | });
258 | 
259 | describe("<Link /> with `asChild` prop", () => {
260 |   it("when `asChild` is not specified, wraps the children in an <a />", () => {
261 |     const { getByText } = render(
262 |       <Link href="/about">
263 |         <div className="link--wannabe">Click Me</div>
264 |       </Link>
265 |     );
266 | 
267 |     const link = getByText("Click Me");
268 | 
269 |     expect(link.tagName).toBe("DIV");
270 |     expect(link).not.toHaveAttribute("href");
271 |     expect(link).toHaveClass("link--wannabe");
272 |     expect(link).toHaveTextContent("Click Me");
273 | 
274 |     expect(link.parentElement?.tagName).toBe("A");
275 |     expect(link.parentElement).toHaveAttribute("href", "/about");
276 |   });
277 | 
278 |   it("when invalid element is provided, wraps the children in an <a />", () => {
279 |     const { getByText } = render(
280 |       /* @ts-expect-error */
281 |       <Link href="/about" asChild>
282 |         Click Me
283 |       </Link>
284 |     );
285 | 
286 |     const link = getByText("Click Me");
287 | 
288 |     expect(link.tagName).toBe("A");
289 |     expect(link).toHaveAttribute("href", "/about");
290 |     expect(link).toHaveTextContent("Click Me");
291 |   });
292 | 
293 |   it("when more than one element is provided, wraps the children in an <a />", async () => {
294 |     const { getByText } = render(
295 |       /* @ts-expect-error */
296 |       <Link href="/about" asChild>
297 |         <span>1</span>
298 |         <span>2</span>
299 |         <span>3</span>
300 |       </Link>
301 |     );
302 | 
303 |     const span = getByText("1");
304 | 
305 |     expect(span.parentElement?.tagName).toBe("A");
306 | 
307 |     expect(span.parentElement).toHaveAttribute("href", "/about");
308 |     expect(span.parentElement).toHaveTextContent("123");
309 |   });
310 | 
311 |   it("injects href prop when rendered with `asChild`", () => {
312 |     const { getByText } = render(
313 |       <Link href="/about" asChild>
314 |         <div className="link--wannabe">Click Me</div>
315 |       </Link>
316 |     );
317 | 
318 |     const link = getByText("Click Me");
319 | 
320 |     expect(link.tagName).toBe("DIV");
321 |     expect(link).toHaveClass("link--wannabe");
322 |     expect(link).toHaveAttribute("href", "/about");
323 |     expect(link).toHaveTextContent("Click Me");
324 |   });
325 | 
326 |   it("missing href or to won't crash", () => {
327 |     const { getByText } = render(
328 |       /* @ts-expect-error */
329 |       <Link>Click Me</Link>
330 |     );
331 | 
332 |     const link = getByText("Click Me");
333 | 
334 |     expect(link.tagName).toBe("A");
335 |     expect(link).toHaveAttribute("href", undefined);
336 |     expect(link).toHaveTextContent("Click Me");
337 |   });
338 | });
339 | 


--------------------------------------------------------------------------------
/packages/wouter/test/location-hook.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, expectTypeOf, describe } from "vitest";
 2 | import { BaseLocationHook, HookNavigationOptions } from "wouter";
 3 | 
 4 | describe("`HookNavigationOptions` utility type", () => {
 5 |   it("should return empty interface for hooks with no nav options", () => {
 6 |     const hook = (): [string, (path: string) => void] => {
 7 |       return ["stub", (path: string) => {}];
 8 |     };
 9 | 
10 |     type Options = HookNavigationOptions<typeof hook>;
11 | 
12 |     expectTypeOf<Options>().toEqualTypeOf<{}>();
13 | 
14 |     const optionsExt: Options | { a: 1 } = { a: 1, b: 2 };
15 |   });
16 | 
17 |   it("should return object with required navigation params", () => {
18 |     const hook = (): [
19 |       string,
20 |       (path: string, options: { replace: boolean; optional?: number }) => void
21 |     ] => {
22 |       return ["stub", () => {}];
23 |     };
24 | 
25 |     type Options = HookNavigationOptions<typeof hook>;
26 | 
27 |     // @ts-expect-error
28 |     expectTypeOf<Options>().toEqualTypeOf<{
29 |       replace: boolean;
30 |       foo: string;
31 |     }>();
32 | 
33 |     expectTypeOf<Options>().toEqualTypeOf<{
34 |       replace: boolean;
35 |       optional?: number;
36 |     }>();
37 |   });
38 | 
39 |   it("should not contain never when options are optional", () => {
40 |     const hook = (
41 |       param: string
42 |     ): [string, (path: string, options?: { replace: boolean }) => void] => {
43 |       return ["stub", () => {}];
44 |     };
45 | 
46 |     type Options = HookNavigationOptions<typeof hook>;
47 | 
48 |     expectTypeOf<Options>().toEqualTypeOf<{
49 |       replace: boolean;
50 |     }>();
51 |   });
52 | 
53 |   it("should only support valid hooks", () => {
54 |     // @ts-expect-error
55 |     type A = HookNavigationOptions<string>;
56 |     // @ts-expect-error
57 |     type B = HookNavigationOptions<{}>;
58 |     // @ts-expect-error
59 |     type C = HookNavigationOptions<() => []>;
60 |   });
61 | 
62 |   it("should return empty object when `BaseLocationHook` is given", () => {
63 |     type Options = HookNavigationOptions<BaseLocationHook>;
64 |     expectTypeOf<Options>().toEqualTypeOf<{}>();
65 |   });
66 | });
67 | 


--------------------------------------------------------------------------------
/packages/wouter/test/match-route.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, expectTypeOf, assertType } from "vitest";
 2 | import { matchRoute, useRouter } from "wouter";
 3 | 
 4 | const { parser } = useRouter();
 5 | 
 6 | it("should only accept strings", () => {
 7 |   // @ts-expect-error
 8 |   assertType(matchRoute(parser, Symbol(), ""));
 9 |   // @ts-expect-error
10 |   assertType(matchRoute(parser, undefined, ""));
11 |   assertType(matchRoute(parser, "/", ""));
12 | });
13 | 
14 | it('has a boolean "match" result as a first returned value', () => {
15 |   const [match] = matchRoute(parser, "/", "");
16 |   expectTypeOf(match).toEqualTypeOf<boolean>();
17 | });
18 | 
19 | it("returns null as parameters when there was no match", () => {
20 |   const [match, params] = matchRoute(parser, "/foo", "");
21 | 
22 |   if (!match) {
23 |     expectTypeOf(params).toEqualTypeOf<null>();
24 |   }
25 | });
26 | 
27 | it("accepts the type of parameters as a generic argument", () => {
28 |   const [match, params] = matchRoute<{ id: string; name: string | undefined }>(
29 |     parser,
30 |     "/app/users/:name?/:id",
31 |     ""
32 |   );
33 | 
34 |   if (match) {
35 |     expectTypeOf(params).toEqualTypeOf<{
36 |       id: string;
37 |       name: string | undefined;
38 |     }>();
39 |   }
40 | });
41 | 
42 | it("infers parameters from the route path", () => {
43 |   const [, inferedParams] = matchRoute(parser, "/app/users/:name?/:id/*?", "");
44 | 
45 |   if (inferedParams) {
46 |     expectTypeOf(inferedParams).toMatchTypeOf<{
47 |       0?: string;
48 |       1?: string;
49 |       2?: string;
50 |       name?: string;
51 |       id: string;
52 |       wildcard?: string;
53 |     }>();
54 |   }
55 | });
56 | 


--------------------------------------------------------------------------------
/packages/wouter/test/memory-location.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, assertType, expectTypeOf } from "vitest";
 2 | import { memoryLocation } from "wouter/memory-location";
 3 | import { BaseLocationHook } from "wouter";
 4 | 
 5 | it("should return hook that supports location spec", () => {
 6 |   const { hook } = memoryLocation();
 7 | 
 8 |   expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
 9 | 
10 |   const [location, navigate] = hook();
11 | 
12 |   assertType<string>(location);
13 |   assertType<Function>(navigate);
14 | });
15 | 
16 | it("should return `navigate` method for navigating outside of components", () => {
17 |   const { navigate } = memoryLocation();
18 | 
19 |   assertType<Function>(navigate);
20 | });
21 | 
22 | it("should support `record` option for saving the navigation history", () => {
23 |   const { history, reset } = memoryLocation({ record: true });
24 | 
25 |   assertType<string[]>(history);
26 |   assertType<Function>(reset);
27 | });
28 | 
29 | it("should have history only wheen record is true", () => {
30 |   // @ts-expect-error
31 |   const { history, reset } = memoryLocation({ record: false });
32 |   assertType(history);
33 |   assertType(reset);
34 | });
35 | 
36 | it("should support initial path", () => {
37 |   const { hook } = memoryLocation({ path: "/initial-path" });
38 | 
39 |   expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
40 | });
41 | 
42 | it("should support `static` option", () => {
43 |   const { hook } = memoryLocation({ static: true });
44 | 
45 |   expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
46 | });
47 | 


--------------------------------------------------------------------------------
/packages/wouter/test/memory-location.test.ts:
--------------------------------------------------------------------------------
  1 | import { it, expect } from "vitest";
  2 | import { renderHook, act } from "@testing-library/react";
  3 | import { memoryLocation } from "wouter/memory-location";
  4 | 
  5 | it("returns a hook that is compatible with location spec", () => {
  6 |   const { hook } = memoryLocation();
  7 | 
  8 |   const { result, unmount } = renderHook(() => hook());
  9 |   const [value, update] = result.current;
 10 | 
 11 |   expect(typeof value).toBe("string");
 12 |   expect(typeof update).toBe("function");
 13 |   unmount();
 14 | });
 15 | 
 16 | it("should support initial path", () => {
 17 |   const { hook } = memoryLocation({ path: "/test-case" });
 18 | 
 19 |   const { result, unmount } = renderHook(() => hook());
 20 |   const [value] = result.current;
 21 | 
 22 |   expect(value).toBe("/test-case");
 23 |   unmount();
 24 | });
 25 | 
 26 | it("should support initial path with query", () => {
 27 |   const { searchHook } = memoryLocation({ path: "/test-case?foo=bar" });
 28 | 
 29 |   const { result, unmount } = renderHook(() => searchHook());
 30 |   const value = result.current;
 31 | 
 32 |   expect(value).toBe("foo=bar");
 33 |   unmount();
 34 | });
 35 | 
 36 | it("should support search path as parameter", () => {
 37 |   const { searchHook } = memoryLocation({
 38 |     path: "/test-case?foo=bar",
 39 |     searchPath: "key=value",
 40 |   });
 41 | 
 42 |   const { result, unmount } = renderHook(() => searchHook());
 43 |   const value = result.current;
 44 | 
 45 |   expect(value).toBe("foo=bar&key=value");
 46 |   unmount();
 47 | });
 48 | 
 49 | it('should return location hook that has initial path "/" by default', () => {
 50 |   const { hook } = memoryLocation();
 51 | 
 52 |   const { result, unmount } = renderHook(() => hook());
 53 |   const [value] = result.current;
 54 | 
 55 |   expect(value).toBe("/");
 56 |   unmount();
 57 | });
 58 | 
 59 | it('should return search hook that has initial query "" by default', () => {
 60 |   const { searchHook } = memoryLocation();
 61 | 
 62 |   const { result, unmount } = renderHook(() => searchHook());
 63 |   const value = result.current;
 64 | 
 65 |   expect(value).toBe("");
 66 |   unmount();
 67 | });
 68 | 
 69 | it("should return standalone `navigate` method", () => {
 70 |   const { hook, navigate } = memoryLocation();
 71 | 
 72 |   const { result, unmount } = renderHook(() => hook());
 73 | 
 74 |   act(() => navigate("/standalone"));
 75 | 
 76 |   const [value] = result.current;
 77 |   expect(value).toBe("/standalone");
 78 |   unmount();
 79 | });
 80 | 
 81 | it("should return location hook that supports navigation", () => {
 82 |   const { hook } = memoryLocation();
 83 | 
 84 |   const { result, unmount } = renderHook(() => hook());
 85 | 
 86 |   act(() => result.current[1]("/location"));
 87 | 
 88 |   const [value] = result.current;
 89 |   expect(value).toBe("/location");
 90 |   unmount();
 91 | });
 92 | 
 93 | it("should record all history when `record` option is provided", () => {
 94 |   const {
 95 |     hook,
 96 |     history,
 97 |     navigate: standalone,
 98 |   } = memoryLocation({ record: true, path: "/test" });
 99 | 
100 |   const { result, unmount } = renderHook(() => hook());
101 | 
102 |   act(() => standalone("/standalone"));
103 |   act(() => result.current[1]("/location"));
104 | 
105 |   expect(result.current[0]).toBe("/location");
106 | 
107 |   expect(history).toStrictEqual(["/test", "/standalone", "/location"]);
108 | 
109 |   act(() => standalone("/standalone", { replace: true }));
110 | 
111 |   expect(history).toStrictEqual(["/test", "/standalone", "/standalone"]);
112 | 
113 |   act(() => result.current[1]("/location", { replace: true }));
114 | 
115 |   expect(history).toStrictEqual(["/test", "/standalone", "/location"]);
116 | 
117 |   unmount();
118 | });
119 | 
120 | it("should not have history when `record` option is falsy", () => {
121 |   // @ts-expect-error
122 |   const { history, reset } = memoryLocation();
123 |   expect(history).not.toBeDefined();
124 |   expect(reset).not.toBeDefined();
125 | });
126 | 
127 | it("should have reset method when `record` option is provided", () => {
128 |   const { history, reset, navigate } = memoryLocation({
129 |     path: "/initial",
130 |     record: true,
131 |   });
132 |   expect(history).toBeDefined();
133 |   expect(reset).toBeDefined();
134 | 
135 |   navigate("test-1");
136 |   navigate("test-2");
137 | 
138 |   reset();
139 | 
140 |   expect(history).toStrictEqual(["/initial"]);
141 | });
142 | 
143 | it("should have reset method that reset hook location", () => {
144 |   const { hook, history, navigate, reset } = memoryLocation({
145 |     record: true,
146 |     path: "/test",
147 |   });
148 |   const { result, unmount } = renderHook(() => hook());
149 | 
150 |   act(() => navigate("/location"));
151 | 
152 |   expect(result.current[0]).toBe("/location");
153 | 
154 |   expect(history).toStrictEqual(["/test", "/location"]);
155 | 
156 |   act(() => reset());
157 | 
158 |   expect(history).toStrictEqual(["/test"]);
159 | 
160 |   expect(result.current[0]).toBe("/test");
161 | 
162 |   unmount();
163 | });
164 | 


--------------------------------------------------------------------------------
/packages/wouter/test/nested-route.test.tsx:
--------------------------------------------------------------------------------
  1 | import { it, expect, describe } from "vitest";
  2 | import { act, render, renderHook } from "@testing-library/react";
  3 | 
  4 | import { Route, Router, Switch, useRouter } from "wouter";
  5 | import { memoryLocation } from "wouter/memory-location";
  6 | 
  7 | describe("when `nest` prop is given", () => {
  8 |   it("renders by default", () => {
  9 |     const { container } = render(<Route nest>matched!</Route>);
 10 |     expect(container.innerHTML).toBe("matched!");
 11 |   });
 12 | 
 13 |   it("matches the pattern loosely", () => {
 14 |     const { hook, navigate } = memoryLocation();
 15 | 
 16 |     const { container } = render(
 17 |       <Router hook={hook}>
 18 |         <Route path="/posts/:slug" nest>
 19 |           matched!
 20 |         </Route>
 21 |       </Router>
 22 |     );
 23 | 
 24 |     expect(container.innerHTML).toBe("");
 25 | 
 26 |     act(() => navigate("/posts/all")); // full match
 27 |     expect(container.innerHTML).toBe("matched!");
 28 | 
 29 |     act(() => navigate("/users"));
 30 |     expect(container.innerHTML).toBe("");
 31 | 
 32 |     act(() => navigate("/posts/10-react-tricks/table-of-contents"));
 33 |     expect(container.innerHTML).toBe("matched!");
 34 |   });
 35 | 
 36 |   it("can be used inside a Switch", () => {
 37 |     const { container } = render(
 38 |       <Router
 39 |         hook={
 40 |           memoryLocation({ path: "/posts/13/2012/sort", static: true }).hook
 41 |         }
 42 |       >
 43 |         <Switch>
 44 |           <Route path="/about">about</Route>
 45 |           <Route path="/posts/:slug" nest>
 46 |             nested
 47 |           </Route>
 48 |           <Route>default</Route>
 49 |         </Switch>
 50 |       </Router>
 51 |     );
 52 | 
 53 |     expect(container.innerHTML).toBe("nested");
 54 |   });
 55 | 
 56 |   it("sets the base to the matched segment", () => {
 57 |     const { result } = renderHook(() => useRouter().base, {
 58 |       wrapper: (props) => (
 59 |         <Router
 60 |           hook={memoryLocation({ path: "/2012/04/posts", static: true }).hook}
 61 |         >
 62 |           <Route path="/:year/:month" nest>
 63 |             <Route path="/posts">{props.children}</Route>
 64 |           </Route>
 65 |         </Router>
 66 |       ),
 67 |     });
 68 | 
 69 |     expect(result.current).toBe("/2012/04");
 70 |   });
 71 | 
 72 |   it("can be nested in another nested `Route` or `Router`", () => {
 73 |     const { container } = render(
 74 |       <Router
 75 |         base="/app"
 76 |         hook={
 77 |           memoryLocation({
 78 |             path: "/app/users/alexey/settings/all",
 79 |             static: true,
 80 |           }).hook
 81 |         }
 82 |       >
 83 |         <Route path="/users/:name" nest>
 84 |           <Route path="/settings">should not be rendered</Route>
 85 | 
 86 |           <Route path="/settings" nest>
 87 |             <Route path="/all">All settings</Route>
 88 |           </Route>
 89 |         </Route>
 90 |       </Router>
 91 |     );
 92 | 
 93 |     expect(container.innerHTML).toBe("All settings");
 94 |   });
 95 | 
 96 |   it("reacts to `nest` updates", () => {
 97 |     const { hook } = memoryLocation({
 98 |       path: "/app/apple/products",
 99 |       static: true,
100 |     });
101 | 
102 |     const App = ({ nested }: { nested: boolean }) => {
103 |       return (
104 |         <Router hook={hook}>
105 |           <Route path="/app/:company" nest={nested}>
106 |             matched!
107 |           </Route>
108 |         </Router>
109 |       );
110 |     };
111 | 
112 |     const { container, rerender } = render(<App nested={true} />);
113 |     expect(container.innerHTML).toBe("matched!");
114 | 
115 |     rerender(<App nested={false} />);
116 |     expect(container.innerHTML).toBe("");
117 |   });
118 | 
119 |   it("works with one optional segment", () => {
120 |     const { hook, navigate } = memoryLocation({
121 |       path: "/",
122 |     });
123 | 
124 |     const App = () => {
125 |       return (
126 |         <Router hook={hook}>
127 |           <Route path="/:version?" nest>
128 |             {({ version }) => version ?? "default"}
129 |           </Route>
130 |         </Router>
131 |       );
132 |     };
133 | 
134 |     const { container } = render(<App />);
135 |     expect(container.innerHTML).toBe("default");
136 | 
137 |     act(() => navigate("/v1"));
138 |     expect(container.innerHTML).toBe("v1");
139 | 
140 |     act(() => navigate("/v2/dashboard"));
141 |     expect(container.innerHTML).toBe("v2");
142 |   });
143 | });
144 | 


--------------------------------------------------------------------------------
/packages/wouter/test/parser.test.tsx:
--------------------------------------------------------------------------------
 1 | import { it, expect } from "vitest";
 2 | 
 3 | import { pathToRegexp, Key } from "path-to-regexp";
 4 | import { renderHook } from "@testing-library/react";
 5 | 
 6 | import { Router, useRouter, useRoute, Parser } from "wouter";
 7 | import { memoryLocation } from "wouter/memory-location";
 8 | 
 9 | // Custom parser that uses `path-to-regexp` instead of `regexparam`
10 | const pathToRegexpParser: Parser = (route: string) => {
11 |   const keys: Key[] = [];
12 |   const pattern = pathToRegexp(route, keys);
13 | 
14 |   return { pattern, keys: keys.map((k) => String(k.name)) };
15 | };
16 | 
17 | it("overrides the `parser` prop on the current router", () => {
18 |   const { result } = renderHook(() => useRouter(), {
19 |     wrapper: ({ children }) => (
20 |       <Router parser={pathToRegexpParser}>{children}</Router>
21 |     ),
22 |   });
23 | 
24 |   const router = result.current;
25 |   expect(router.parser).toBe(pathToRegexpParser);
26 | });
27 | 
28 | it("allows to change the behaviour of route matching", () => {
29 |   const { result } = renderHook(
30 |     () => useRoute("/(home|dashboard)/:pages?/users/:rest*"),
31 |     {
32 |       wrapper: ({ children }) => (
33 |         <Router
34 |           hook={memoryLocation({ path: "/home/users/10/bio" }).hook}
35 |           parser={pathToRegexpParser}
36 |         >
37 |           {children}
38 |         </Router>
39 |       ),
40 |     }
41 |   );
42 | 
43 |   expect(result.current).toStrictEqual([
44 |     true,
45 |     { 0: "home", 1: undefined, 2: "10/bio", pages: undefined, rest: "10/bio" },
46 |   ]);
47 | });
48 | 


--------------------------------------------------------------------------------
/packages/wouter/test/redirect.test-d.tsx:
--------------------------------------------------------------------------------
 1 | import { describe, it, assertType } from "vitest";
 2 | import { Redirect } from "wouter";
 3 | 
 4 | describe("Redirect types", () => {
 5 |   it("should have required prop href", () => {
 6 |     // @ts-expect-error
 7 |     assertType(<Redirect />);
 8 |     assertType(<Redirect href="/" />);
 9 |   });
10 | 
11 |   it("should support state prop", () => {
12 |     assertType(<Redirect href="/" state={{ a: "foo" }} />);
13 |     assertType(<Redirect href="/" state={null} />);
14 |     assertType(<Redirect href="/" state={undefined} />);
15 |     assertType(<Redirect href="/" state="string" />);
16 |   });
17 | 
18 |   it("always renders nothing", () => {
19 |     // can be used in JSX
20 |     <div>
21 |       <Redirect href="/" />
22 |     </div>;
23 | 
24 |     assertType<null>(Redirect({ href: "/" }));
25 |   });
26 | 
27 |   it("can not accept children", () => {
28 |     // @ts-expect-error
29 |     <Redirect href="/">hi!</Redirect>;
30 | 
31 |     // prettier-ignore
32 |     // @ts-expect-error
33 |     <Redirect href="/"><><div>Fragment</div></></Redirect>;
34 |   });
35 | });
36 | 


--------------------------------------------------------------------------------
/packages/wouter/test/redirect.test.tsx:
--------------------------------------------------------------------------------
 1 | import { it, expect } from "vitest";
 2 | import { render } from "@testing-library/react";
 3 | import { useState } from "react";
 4 | 
 5 | import { Redirect, Router } from "wouter";
 6 | 
 7 | export const customHookWithReturn =
 8 |   (initialPath = "/") =>
 9 |   () => {
10 |     const [path, updatePath] = useState(initialPath);
11 |     const navigate = (path: string) => {
12 |       updatePath(path);
13 |       return "foo";
14 |     };
15 | 
16 |     return [path, navigate];
17 |   };
18 | 
19 | it("renders nothing", () => {
20 |   const { container, unmount } = render(<Redirect to="/users" />);
21 |   expect(container.childNodes.length).toBe(0);
22 |   unmount();
23 | });
24 | 
25 | it("results in change of current location", () => {
26 |   const { unmount } = render(<Redirect to="/users" />);
27 | 
28 |   expect(location.pathname).toBe("/users");
29 |   unmount();
30 | });
31 | 
32 | it("supports `base` routers with relative path", () => {
33 |   const { unmount } = render(
34 |     <Router base="/app">
35 |       <Redirect to="/nested" />
36 |     </Router>
37 |   );
38 | 
39 |   expect(location.pathname).toBe("/app/nested");
40 |   unmount();
41 | });
42 | 
43 | it("supports `base` routers with absolute path", () => {
44 |   const { unmount } = render(
45 |     <Router base="/app">
46 |       <Redirect to="~/absolute" />
47 |     </Router>
48 |   );
49 | 
50 |   expect(location.pathname).toBe("/absolute");
51 |   unmount();
52 | });
53 | 
54 | it("supports replace navigation", () => {
55 |   const histBefore = history.length;
56 | 
57 |   const { unmount } = render(<Redirect to="/users" replace />);
58 | 
59 |   expect(location.pathname).toBe("/users");
60 |   expect(history.length).toBe(histBefore);
61 |   unmount();
62 | });
63 | 
64 | it("supports history state", () => {
65 |   const testState = { hello: "world" };
66 |   const { unmount } = render(<Redirect to="/users" state={testState} />);
67 | 
68 |   expect(location.pathname).toBe("/users");
69 |   expect(history.state).toStrictEqual(testState);
70 |   unmount();
71 | });
72 | 
73 | it("useLayoutEffect should return nothing", () => {
74 |   const { unmount } = render(
75 |     // @ts-expect-error
76 |     <Router hook={customHookWithReturn()}>
77 |       <Redirect to="/users" replace />
78 |     </Router>
79 |   );
80 | 
81 |   expect(location.pathname).toBe("/users");
82 |   unmount();
83 | });
84 | 


--------------------------------------------------------------------------------
/packages/wouter/test/route.test-d.tsx:
--------------------------------------------------------------------------------
  1 | import { it, describe, expectTypeOf, assertType } from "vitest";
  2 | import { Route } from "wouter";
  3 | import { ComponentProps } from "react";
  4 | import * as React from "react";
  5 | 
  6 | describe("`path` prop", () => {
  7 |   it("is optional", () => {
  8 |     assertType(<Route />);
  9 |   });
 10 | 
 11 |   it("should be a string or RegExp", () => {
 12 |     let a: ComponentProps<typeof Route>["path"];
 13 |     expectTypeOf(a).toMatchTypeOf<string | RegExp | undefined>();
 14 |   });
 15 | });
 16 | 
 17 | it("accepts the optional boolean `nest` prop", () => {
 18 |   assertType(<Route nest />);
 19 |   assertType(<Route nest={false} />);
 20 | 
 21 |   // @ts-expect-error - should be boolean
 22 |   assertType(<Route nest={"true"} />);
 23 | });
 24 | 
 25 | it("renders a component provided in the `component` prop", () => {
 26 |   const Header = () => <div />;
 27 |   const Profile = () => null;
 28 | 
 29 |   <Route path="/header" component={Header} />;
 30 |   <Route path="/profile/:id" component={Profile} />;
 31 | 
 32 |   // @ts-expect-error must be a component, not JSX
 33 |   <Route path="/header" component={<a />} />;
 34 | });
 35 | 
 36 | it("accepts class components in the `component` prop", () => {
 37 |   class A extends React.Component<{ params: {} }> {
 38 |     render() {
 39 |       return <div />;
 40 |     }
 41 |   }
 42 | 
 43 |   <Route path="/app" component={A} />;
 44 | });
 45 | 
 46 | it("accepts children", () => {
 47 |   <Route path="/app">
 48 |     <div />
 49 |   </Route>;
 50 | 
 51 |   <Route path="/app">
 52 |     This is a <b>mixed</b> content
 53 |   </Route>;
 54 | 
 55 |   <Route>
 56 |     <>
 57 |       <div />
 58 |     </>
 59 |   </Route>;
 60 | });
 61 | 
 62 | it("supports functions as children", () => {
 63 |   <Route path="/users/:id">
 64 |     {(params) => {
 65 |       expectTypeOf(params).toMatchTypeOf<{}>();
 66 |       return <div />;
 67 |     }}
 68 |   </Route>;
 69 | 
 70 |   <Route path="/users/:id">{({ id }) => `User id: ${id}`}</Route>;
 71 | 
 72 |   <Route path="/users/:id">
 73 |     {({ age }: { age: string }) => `User age: ${age}`}
 74 |   </Route>;
 75 | 
 76 |   // @ts-expect-error function should return valid JSX
 77 |   <Route path="/app">{() => {}}</Route>;
 78 | 
 79 |   // prettier-ignore
 80 |   // @ts-expect-error you can't use JSX together with render function
 81 |   <Route path="/">{() => <div />}<a>Link</a></Route>;
 82 | });
 83 | 
 84 | describe("parameter inference", () => {
 85 |   it("can infer type of params from the path given", () => {
 86 |     <Route path="/path/:first/:second/another/:third">
 87 |       {({ first, second, third }) => {
 88 |         expectTypeOf(first).toEqualTypeOf<string>();
 89 |         return <div>{`${first}, ${second}, ${third}`}</div>;
 90 |       }}
 91 |     </Route>;
 92 | 
 93 |     <Route path="/users/:name/">
 94 |       {/* @ts-expect-error - `age` param is not present in the pattern */}
 95 |       {({ name, age }) => {
 96 |         return <div>{`Hello, ${name}`}</div>;
 97 |       }}
 98 |     </Route>;
 99 |   });
100 | 
101 |   it("extract wildcard params into `wild` property", () => {
102 |     <Route path="/users/*/settings">
103 |       {({ wild }) => {
104 |         expectTypeOf(wild).toEqualTypeOf<string>();
105 |         return <div>The path is {wild}</div>;
106 |       }}
107 |     </Route>;
108 |   });
109 | 
110 |   it("allows to customize type of params via generic parameter", () => {
111 |     <Route<{ name: string; lastName: string }> path="/users/:name/:age">
112 |       {(params) => {
113 |         expectTypeOf(params.lastName).toEqualTypeOf<string>();
114 |         return <div>This really is undefined {params.lastName}</div>;
115 |       }}
116 |     </Route>;
117 |   });
118 | 
119 |   it("can't infer the type when the path isn't known at compile time", () => {
120 |     <Route path={JSON.parse('"/home/:section"')}>
121 |       {(params) => {
122 |         // @ts-expect-error
123 |         params.section;
124 |         return <div />;
125 |       }}
126 |     </Route>;
127 |   });
128 | });
129 | 


--------------------------------------------------------------------------------
/packages/wouter/test/route.test.tsx:
--------------------------------------------------------------------------------
  1 | import { it, expect, afterEach } from "vitest";
  2 | import { render, act, cleanup } from "@testing-library/react";
  3 | 
  4 | import { Router, Route } from "wouter";
  5 | import { memoryLocation } from "wouter/memory-location";
  6 | import { ReactElement } from "react";
  7 | 
  8 | // Clean up after each test to avoid DOM pollution
  9 | afterEach(cleanup);
 10 | 
 11 | const testRouteRender = (initialPath: string, jsx: ReactElement) => {
 12 |   return render(
 13 |     <Router hook={memoryLocation({ path: initialPath }).hook}>{jsx}</Router>
 14 |   );
 15 | };
 16 | 
 17 | it("always renders its content when `path` is empty", () => {
 18 |   const { container } = testRouteRender(
 19 |     "/nothing",
 20 |     <Route>
 21 |       <h1>Hello!</h1>
 22 |     </Route>
 23 |   );
 24 | 
 25 |   const heading = container.querySelector("h1");
 26 |   expect(heading).toBeInTheDocument();
 27 |   expect(heading).toHaveTextContent("Hello!");
 28 | });
 29 | 
 30 | it("accepts plain children", () => {
 31 |   const { container } = testRouteRender(
 32 |     "/foo",
 33 |     <Route path="/foo">
 34 |       <h1>Hello!</h1>
 35 |     </Route>
 36 |   );
 37 | 
 38 |   const heading = container.querySelector("h1");
 39 |   expect(heading).toBeInTheDocument();
 40 |   expect(heading).toHaveTextContent("Hello!");
 41 | });
 42 | 
 43 | it("works with render props", () => {
 44 |   const { container } = testRouteRender(
 45 |     "/foo",
 46 |     <Route path="/foo">{() => <h1>Hello!</h1>}</Route>
 47 |   );
 48 | 
 49 |   const heading = container.querySelector("h1");
 50 |   expect(heading).toBeInTheDocument();
 51 |   expect(heading).toHaveTextContent("Hello!");
 52 | });
 53 | 
 54 | it("passes a match param object to the render function", () => {
 55 |   const { container } = testRouteRender(
 56 |     "/users/alex",
 57 |     <Route path="/users/:name">{(params) => <h1>{params.name}</h1>}</Route>
 58 |   );
 59 | 
 60 |   const heading = container.querySelector("h1");
 61 |   expect(heading).toBeInTheDocument();
 62 |   expect(heading).toHaveTextContent("alex");
 63 | });
 64 | 
 65 | it("renders nothing when there is not match", () => {
 66 |   const { container } = testRouteRender(
 67 |     "/bar",
 68 |     <Route path="/foo">
 69 |       <div>Hi!</div>
 70 |     </Route>
 71 |   );
 72 | 
 73 |   expect(container.querySelector("div")).not.toBeInTheDocument();
 74 | });
 75 | 
 76 | it("supports `component` prop similar to React-Router", () => {
 77 |   const Users = () => <h2>All users</h2>;
 78 | 
 79 |   const { container } = testRouteRender(
 80 |     "/foo",
 81 |     <Route path="/foo" component={Users} />
 82 |   );
 83 | 
 84 |   const heading = container.querySelector("h2");
 85 |   expect(heading).toBeInTheDocument();
 86 |   expect(heading).toHaveTextContent("All users");
 87 | });
 88 | 
 89 | it("supports `base` routers with relative path", () => {
 90 |   const { container, unmount } = render(
 91 |     <Router base="/app">
 92 |       <Route path="/nested">
 93 |         <h1>Nested</h1>
 94 |       </Route>
 95 |       <Route path="~/absolute">
 96 |         <h2>Absolute</h2>
 97 |       </Route>
 98 |     </Router>
 99 |   );
100 | 
101 |   act(() => history.replaceState(null, "", "/app/nested"));
102 | 
103 |   expect(container.children).toHaveLength(1);
104 |   expect(container.firstChild).toHaveProperty("tagName", "H1");
105 | 
106 |   unmount();
107 | });
108 | 
109 | it("supports `path` prop with regex", () => {
110 |   const { container } = testRouteRender(
111 |     "/foo",
112 |     <Route path={/[/]foo/}>
113 |       <h1>Hello!</h1>
114 |     </Route>
115 |   );
116 | 
117 |   const heading = container.querySelector("h1");
118 |   expect(heading).toBeInTheDocument();
119 |   expect(heading).toHaveTextContent("Hello!");
120 | });
121 | 
122 | it("supports regex path named params", () => {
123 |   const { container } = testRouteRender(
124 |     "/users/alex",
125 |     <Route path={/[/]users[/](?<name>[a-z]+)/}>
126 |       {(params) => <h1>{params.name}</h1>}
127 |     </Route>
128 |   );
129 | 
130 |   const heading = container.querySelector("h1");
131 |   expect(heading).toBeInTheDocument();
132 |   expect(heading).toHaveTextContent("alex");
133 | });
134 | 
135 | it("supports regex path anonymous params", () => {
136 |   const { container } = testRouteRender(
137 |     "/users/alex",
138 |     <Route path={/[/]users[/]([a-z]+)/}>
139 |       {(params) => <h1>{params[0]}</h1>}
140 |     </Route>
141 |   );
142 | 
143 |   const heading = container.querySelector("h1");
144 |   expect(heading).toBeInTheDocument();
145 |   expect(heading).toHaveTextContent("alex");
146 | });
147 | 
148 | it("rejects when a path does not match the regex", () => {
149 |   const { container } = testRouteRender(
150 |     "/users/1234",
151 |     <Route path={/[/]users[/](?<name>[a-z]+)/}>
152 |       {(params) => <h1>{params.name}</h1>}
153 |     </Route>
154 |   );
155 | 
156 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
157 | });
158 | 


--------------------------------------------------------------------------------
/packages/wouter/test/router.test-d.tsx:
--------------------------------------------------------------------------------
 1 | import { ComponentProps } from "react";
 2 | import { it, expectTypeOf } from "vitest";
 3 | import {
 4 |   Router,
 5 |   Route,
 6 |   BaseLocationHook,
 7 |   useRouter,
 8 |   Parser,
 9 |   Path,
10 | } from "wouter";
11 | 
12 | it("should have at least one child", () => {
13 |   // @ts-expect-error
14 |   <Router />;
15 | });
16 | 
17 | it("accepts valid elements as children", () => {
18 |   const Header = ({ title }: { title: string }) => <h1>{title}</h1>;
19 | 
20 |   <Router>
21 |     <Route path="/" />
22 |     <b>Hello!</b>
23 |   </Router>;
24 | 
25 |   <Router>
26 |     Hello, we have <Header title="foo" /> and some {1337} numbers here.
27 |   </Router>;
28 | 
29 |   <Router>
30 |     <>Fragments!</>
31 |   </Router>;
32 | 
33 |   <Router>
34 |     {/* @ts-expect-error should be a valid element */}
35 |     {() => <div />}
36 |   </Router>;
37 | });
38 | 
39 | it("can be customized with router properties passed as props", () => {
40 |   // @ts-expect-error
41 |   <Router hook="wat?" />;
42 | 
43 |   const useFakeLocation: BaseLocationHook = () => ["/foo", () => {}];
44 |   <Router hook={useFakeLocation}>this is a valid router</Router>;
45 | 
46 |   let fn: ComponentProps<typeof Router>["hook"];
47 |   expectTypeOf(fn).exclude<undefined>().toBeFunction();
48 | 
49 |   <Router base="/app">Hello World!</Router>;
50 | 
51 |   <Router ssrPath="/foo">SSR</Router>;
52 | 
53 |   <Router base="/users" ssrPath="/users/all" hook={useFakeLocation}>
54 |     Custom
55 |   </Router>;
56 | });
57 | 
58 | it("accepts `hrefs` function for transforming href strings", () => {
59 |   const router = useRouter();
60 |   expectTypeOf(router.hrefs).toBeFunction();
61 | 
62 |   <Router hrefs={(href: string) => href + "1"}>0</Router>;
63 | 
64 |   <Router
65 |     hrefs={(href, router) => {
66 |       expectTypeOf(router).toEqualTypeOf<typeof router>();
67 |       return href + router.base;
68 |     }}
69 |   >
70 |     routers as a second argument
71 |   </Router>;
72 | });
73 | 
74 | it("accepts `parser` function for generating regular expressions", () => {
75 |   const parser: Parser = (path: Path, loose?: boolean) => {
76 |     return {
77 |       pattern: new RegExp(`^${path}${loose === true ? "(?=$|[/])" : "[/]
quot;}`),
78 |       keys: [],
79 |     };
80 |   };
81 | 
82 |   <Router parser={parser}>this is a valid router</Router>;
83 | });
84 | 
85 | it("does not accept other props", () => {
86 |   const router = useRouter();
87 | 
88 |   // @ts-expect-error `parent` prop isn't defined
89 |   <Router parent={router}>Parent router</Router>;
90 | });
91 | 


--------------------------------------------------------------------------------
/packages/wouter/test/router.test.tsx:
--------------------------------------------------------------------------------
  1 | import { memo, ReactElement, cloneElement, ComponentProps } from "react";
  2 | import { renderHook, render } from "@testing-library/react";
  3 | import { it, expect, describe } from "vitest";
  4 | import {
  5 |   Router,
  6 |   DefaultParams,
  7 |   useRouter,
  8 |   Parser,
  9 |   BaseLocationHook,
 10 | } from "wouter";
 11 | 
 12 | it("creates a router object on demand", () => {
 13 |   const { result } = renderHook(() => useRouter());
 14 |   expect(result.current).toBeInstanceOf(Object);
 15 | });
 16 | 
 17 | it("creates a router object only once", () => {
 18 |   const { result, rerender } = renderHook(() => useRouter());
 19 |   const router = result.current;
 20 | 
 21 |   rerender();
 22 |   expect(result.current).toBe(router);
 23 | });
 24 | 
 25 | it("does not create new router when <Router /> rerenders", () => {
 26 |   const { result, rerender } = renderHook(() => useRouter(), {
 27 |     wrapper: (props) => <Router>{props.children}</Router>,
 28 |   });
 29 |   const router = result.current;
 30 | 
 31 |   rerender();
 32 |   expect(result.current).toBe(router);
 33 | });
 34 | 
 35 | it("alters the current router with `parser` and `hook` options", () => {
 36 |   const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] });
 37 |   const hook: BaseLocationHook = () => ["/foo", () => {}];
 38 | 
 39 |   const { result } = renderHook(() => useRouter(), {
 40 |     wrapper: (props) => (
 41 |       <Router parser={newParser} hook={hook}>
 42 |         {props.children}
 43 |       </Router>
 44 |     ),
 45 |   });
 46 |   const router = result.current;
 47 | 
 48 |   expect(router).toBeInstanceOf(Object);
 49 |   expect(router.parser).toBe(newParser);
 50 |   expect(router.hook).toBe(hook);
 51 | });
 52 | 
 53 | it("accepts `ssrPath` and `ssrSearch` params", () => {
 54 |   const { result } = renderHook(() => useRouter(), {
 55 |     wrapper: (props) => (
 56 |       <Router ssrPath="/users" ssrSearch="a=b&c=d">
 57 |         {props.children}
 58 |       </Router>
 59 |     ),
 60 |   });
 61 | 
 62 |   expect(result.current.ssrPath).toBe("/users");
 63 |   expect(result.current.ssrSearch).toBe("a=b&c=d");
 64 | });
 65 | 
 66 | it("can extract `ssrSearch` from `ssrPath` after the '?' symbol", () => {
 67 |   let ssrPath: string | undefined = "/no-search";
 68 |   let ssrSearch: string | undefined = undefined;
 69 | 
 70 |   const { result, rerender } = renderHook(() => useRouter(), {
 71 |     wrapper: (props) => (
 72 |       <Router ssrPath={ssrPath} ssrSearch={ssrSearch}>
 73 |         {props.children}
 74 |       </Router>
 75 |     ),
 76 |   });
 77 | 
 78 |   expect(result.current.ssrPath).toBe("/no-search");
 79 |   expect(result.current.ssrSearch).toBe(undefined);
 80 | 
 81 |   ssrPath = "/with-search?a=b&c=d";
 82 |   rerender();
 83 | 
 84 |   expect(result.current.ssrPath).toBe("/with-search");
 85 |   expect(result.current.ssrSearch).toBe("a=b&c=d");
 86 | 
 87 |   ssrSearch = "x=y&z=w";
 88 |   rerender();
 89 |   expect(result.current.ssrSearch).toBe("a=b&c=d");
 90 | });
 91 | 
 92 | it("shares one router instance between components", () => {
 93 |   const routers: any[] = [];
 94 | 
 95 |   const RouterGetter = ({ index }: { index: number }) => {
 96 |     const router = useRouter();
 97 |     routers[index] = router;
 98 |     return <div data-testid={`router-${index}`} />;
 99 |   };
100 | 
101 |   render(
102 |     <>
103 |       <RouterGetter index={0} />
104 |       <RouterGetter index={1} />
105 |       <RouterGetter index={2} />
106 |       <RouterGetter index={3} />
107 |     </>
108 |   );
109 | 
110 |   const uniqRouters = [...new Set<DefaultParams>(routers)];
111 |   expect(uniqRouters.length).toBe(1);
112 | });
113 | 
114 | describe("`base` prop", () => {
115 |   it("is an empty string by default", () => {
116 |     const { result } = renderHook(() => useRouter());
117 |     expect(result.current.base).toBe("");
118 |   });
119 | 
120 |   it("can be customized via the `base` prop", () => {
121 |     const { result } = renderHook(() => useRouter(), {
122 |       wrapper: (props) => <Router base="/foo">{props.children}</Router>,
123 |     });
124 |     expect(result.current.base).toBe("/foo");
125 |   });
126 | 
127 |   it("appends provided path to the parent router's base", () => {
128 |     const { result } = renderHook(() => useRouter(), {
129 |       wrapper: (props) => (
130 |         <Router base="/baz">
131 |           <Router base="/foo">
132 |             <Router base="/bar">{props.children}</Router>
133 |           </Router>
134 |         </Router>
135 |       ),
136 |     });
137 |     expect(result.current.base).toBe("/baz/foo/bar");
138 |   });
139 | });
140 | 
141 | describe("`hook` prop", () => {
142 |   it("when provided, the router isn't inherited from the parent", () => {
143 |     const customHook: BaseLocationHook = () => ["/foo", () => {}];
144 |     const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] });
145 | 
146 |     const {
147 |       result: { current: router },
148 |     } = renderHook(() => useRouter(), {
149 |       wrapper: (props) => (
150 |         <Router base="/app" parser={newParser}>
151 |           <Router hook={customHook} base="/bar">
152 |             {props.children}
153 |           </Router>
154 |         </Router>
155 |       ),
156 |     });
157 | 
158 |     expect(router.hook).toBe(customHook);
159 |     expect(router.parser).not.toBe(newParser);
160 |     expect(router.base).toBe("/bar");
161 |   });
162 | });
163 | 
164 | describe("`hrefs` prop", () => {
165 |   it("sets the router's `hrefs` property", () => {
166 |     const formatter = () => "noop";
167 | 
168 |     const {
169 |       result: { current: router },
170 |     } = renderHook(() => useRouter(), {
171 |       wrapper: (props) => <Router hrefs={formatter}>{props.children}</Router>,
172 |     });
173 | 
174 |     expect(router.hrefs).toBe(formatter);
175 |   });
176 | 
177 |   it("can infer `hrefs` from the `hook`", () => {
178 |     const hookHrefs = () => "noop";
179 |     const hook = (): [string, (v: string) => void] => {
180 |       return ["/foo", () => {}];
181 |     };
182 | 
183 |     hook.hrefs = hookHrefs;
184 | 
185 |     let hrefsRouterOption: ((href: string) => string) | undefined;
186 | 
187 |     const { rerender, result } = renderHook(() => useRouter(), {
188 |       wrapper: (props) => (
189 |         <Router hook={hook} hrefs={hrefsRouterOption}>
190 |           {props.children}
191 |         </Router>
192 |       ),
193 |     });
194 | 
195 |     expect(result.current.hrefs).toBe(hookHrefs);
196 | 
197 |     // `hrefs` passed directly to the router should take precedence
198 |     hrefsRouterOption = (href) => "custom formatter";
199 |     rerender();
200 | 
201 |     expect(result.current.hrefs).toBe(hrefsRouterOption);
202 |   });
203 | });
204 | 
205 | it("updates the context when settings are changed", () => {
206 |   const state: { renders: number } & Partial<ComponentProps<typeof Router>> = {
207 |     renders: 0,
208 |   };
209 | 
210 |   const Memoized = memo((props) => {
211 |     const router = useRouter();
212 |     state.renders++;
213 | 
214 |     state.hook = router.hook;
215 |     state.base = router.base;
216 | 
217 |     return <></>;
218 |   });
219 | 
220 |   const { rerender } = render(
221 |     <Router base="/app">
222 |       <Memoized />
223 |     </Router>
224 |   );
225 | 
226 |   expect(state.renders).toEqual(1);
227 |   expect(state.base).toBe("/app");
228 | 
229 |   rerender(
230 |     <Router base="/app">
231 |       <Memoized />
232 |     </Router>
233 |   );
234 |   expect(state.renders).toEqual(1); // nothing changed
235 | 
236 |   // should re-render the hook
237 |   const newHook: BaseLocationHook = () => ["/location", () => {}];
238 |   rerender(
239 |     <Router hook={newHook} base="/app">
240 |       <Memoized />
241 |     </Router>
242 |   );
243 |   expect(state.renders).toEqual(2);
244 |   expect(state.base).toEqual("/app");
245 |   expect(state.hook).toEqual(newHook);
246 | 
247 |   // should update the context when the base changes as well
248 |   rerender(
249 |     <Router hook={newHook} base="">
250 |       <Memoized />
251 |     </Router>
252 |   );
253 |   expect(state.renders).toEqual(3);
254 |   expect(state.base).toEqual("");
255 |   expect(state.hook).toEqual(newHook);
256 | 
257 |   // the last check that the router context is stable during re-renders
258 |   rerender(
259 |     <Router hook={newHook} base="">
260 |       <Memoized />
261 |     </Router>
262 |   );
263 |   expect(state.renders).toEqual(3); // nothing changed
264 | });
265 | 


--------------------------------------------------------------------------------
/packages/wouter/test/ssr.test.tsx:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @vitest-environment node
  3 |  */
  4 | 
  5 | import { it, expect, describe } from "vitest";
  6 | import { renderToStaticMarkup } from "react-dom/server";
  7 | import {
  8 |   Route,
  9 |   Router,
 10 |   useRoute,
 11 |   Link,
 12 |   Redirect,
 13 |   useSearch,
 14 |   useLocation,
 15 |   SsrContext,
 16 | } from "wouter";
 17 | 
 18 | describe("server-side rendering", () => {
 19 |   it("works via `ssrPath` prop", () => {
 20 |     const App = () => (
 21 |       <Router ssrPath="/users/baz">
 22 |         <Route path="/users/baz">foo</Route>
 23 |         <Route path="/users/:any*">bar</Route>
 24 |         <Route path="/users/:id">{(params) => params.id}</Route>
 25 |         <Route path="/about">should not be rendered</Route>
 26 |       </Router>
 27 |     );
 28 | 
 29 |     const rendered = renderToStaticMarkup(<App />);
 30 |     expect(rendered).toBe("foobarbaz");
 31 |   });
 32 | 
 33 |   it("supports hook-based routes", () => {
 34 |     const HookRoute = () => {
 35 |       const [match, params] = useRoute("/pages/:name");
 36 |       return <>{match ? `Welcome to ${params.name}!` : "Not Found!"}</>;
 37 |     };
 38 | 
 39 |     const App = () => (
 40 |       <Router ssrPath="/pages/intro">
 41 |         <HookRoute />
 42 |       </Router>
 43 |     );
 44 | 
 45 |     const rendered = renderToStaticMarkup(<App />);
 46 |     expect(rendered).toBe("Welcome to intro!");
 47 |   });
 48 | 
 49 |   it("renders valid and accessible link elements", () => {
 50 |     const App = () => (
 51 |       <Router ssrPath="/">
 52 |         <Link href="/users/1" title="Profile">
 53 |           Mark
 54 |         </Link>
 55 |       </Router>
 56 |     );
 57 | 
 58 |     const rendered = renderToStaticMarkup(<App />);
 59 |     expect(rendered).toBe(`<a title="Profile" href="/users/1">Mark</a>`);
 60 |   });
 61 | 
 62 |   it("renders redirects however they have effect only on a client-side", () => {
 63 |     const App = () => (
 64 |       <Router ssrPath="/">
 65 |         <Route path="/">
 66 |           <Redirect to="/foo" />
 67 |         </Route>
 68 | 
 69 |         <Route path="/foo">You won't see that in SSR page</Route>
 70 |       </Router>
 71 |     );
 72 | 
 73 |     const rendered = renderToStaticMarkup(<App />);
 74 |     expect(rendered).toBe("");
 75 |   });
 76 | 
 77 |   it("update ssr context", () => {
 78 |     const context: SsrContext = {};
 79 |     const App = () => (
 80 |       <Router ssrPath="/" ssrContext={context}>
 81 |         <Route path="/">
 82 |           <Redirect to="/foo" />
 83 |         </Route>
 84 |       </Router>
 85 |     );
 86 | 
 87 |     renderToStaticMarkup(<App />);
 88 |     expect(context.redirectTo).toBe("/foo");
 89 |   });
 90 | 
 91 |   describe("rendering with given search string", () => {
 92 |     it("is empty when not specified", () => {
 93 |       const PrintSearch = () => <>{useSearch()}</>;
 94 | 
 95 |       const rendered = renderToStaticMarkup(
 96 |         <Router ssrPath="/">
 97 |           <PrintSearch />
 98 |         </Router>
 99 |       );
100 | 
101 |       expect(rendered).toBe("");
102 |     });
103 | 
104 |     it("allows to override search string", () => {
105 |       const App = () => {
106 |         const search = useSearch();
107 |         const [location] = useLocation();
108 | 
109 |         return (
110 |           <>
111 |             {location} filter by {search}
112 |           </>
113 |         );
114 |       };
115 | 
116 |       const rendered = renderToStaticMarkup(
117 |         <Router ssrPath="/catalog" ssrSearch="sort=created_at">
118 |           <App />
119 |         </Router>
120 |       );
121 | 
122 |       expect(rendered).toBe("/catalog filter by sort=created_at");
123 |     });
124 |   });
125 | });
126 | 


--------------------------------------------------------------------------------
/packages/wouter/test/switch.test.tsx:
--------------------------------------------------------------------------------
  1 | import { it, expect, afterEach } from "vitest";
  2 | 
  3 | import { Router, Route, Switch } from "wouter";
  4 | import { memoryLocation } from "wouter/memory-location";
  5 | 
  6 | import { render, act, cleanup } from "@testing-library/react";
  7 | import { PropsWithChildren, ReactElement } from "react";
  8 | 
  9 | // Clean up after each test to avoid DOM pollution
 10 | afterEach(cleanup);
 11 | 
 12 | const raf = () => new Promise((resolve) => requestAnimationFrame(resolve));
 13 | 
 14 | const testRouteRender = (initialPath: string, jsx: ReactElement) => {
 15 |   return render(
 16 |     <Router hook={memoryLocation({ path: initialPath }).hook}>{jsx}</Router>
 17 |   );
 18 | };
 19 | 
 20 | it("works well when nothing is provided", () => {
 21 |   const { container } = testRouteRender("/users/12", <Switch>{null}</Switch>);
 22 |   // When Switch has no matching children, it renders null, so container should be empty
 23 |   expect(container).toBeEmptyDOMElement();
 24 | });
 25 | 
 26 | it("always renders no more than 1 matched children", () => {
 27 |   const { container } = testRouteRender(
 28 |     "/users/12",
 29 |     <Switch>
 30 |       <Route path="/users/home">
 31 |         <h1 />
 32 |       </Route>
 33 |       <Route path="/users/:id">
 34 |         <h2 />
 35 |       </Route>
 36 |       <Route path="/users/:rest*">
 37 |         <h3 />
 38 |       </Route>
 39 |     </Switch>
 40 |   );
 41 | 
 42 |   // Should only render the h2 that matches /users/:id
 43 |   expect(container.querySelectorAll("h1, h2, h3")).toHaveLength(1);
 44 |   expect(container.querySelector("h2")).toBeInTheDocument();
 45 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
 46 |   expect(container.querySelector("h3")).not.toBeInTheDocument();
 47 | });
 48 | 
 49 | it("ignores mixed children", () => {
 50 |   const { container } = testRouteRender(
 51 |     "/users",
 52 |     <Switch>
 53 |       Here is a<Route path="/users">route</Route>
 54 |       route
 55 |     </Switch>
 56 |   );
 57 | 
 58 |   // Should only render the route content, ignoring text nodes
 59 |   expect(container).toHaveTextContent("route");
 60 |   // The text "Here is a" and "route" outside the Route should be ignored
 61 |   expect(container.textContent).toBe("route");
 62 | });
 63 | 
 64 | it("ignores falsy children", () => {
 65 |   const { container } = testRouteRender(
 66 |     "/users",
 67 |     <Switch>
 68 |       {""}
 69 |       {false}
 70 |       {null}
 71 |       {undefined}
 72 |       <Route path="/users">route</Route>
 73 |     </Switch>
 74 |   );
 75 | 
 76 |   // Should only render the route content
 77 |   expect(container).toHaveTextContent("route");
 78 |   expect(container.textContent).toBe("route");
 79 | });
 80 | 
 81 | it("matches regular components as well", () => {
 82 |   const Dummy = (props: PropsWithChildren<{ path: string }>) => (
 83 |     <>{props.children}</>
 84 |   );
 85 | 
 86 |   const { container } = testRouteRender(
 87 |     "/",
 88 |     <Switch>
 89 |       <Dummy path="/">Component</Dummy>
 90 |       <b>Bold</b>
 91 |     </Switch>
 92 |   );
 93 | 
 94 |   // Should render the Dummy component content
 95 |   expect(container).toHaveTextContent("Component");
 96 |   expect(container.querySelector("b")).not.toBeInTheDocument();
 97 | });
 98 | 
 99 | it("allows to specify which routes to render via `location` prop", () => {
100 |   const { container } = testRouteRender(
101 |     "/something-different",
102 |     <Switch location="/users">
103 |       <Route path="/users">route</Route>
104 |     </Switch>
105 |   );
106 | 
107 |   // Should render based on the location prop, not the actual path
108 |   expect(container).toHaveTextContent("route");
109 | });
110 | 
111 | it("always ensures the consistency of inner routes rendering", async () => {
112 |   history.replaceState(null, "", "/foo/bar");
113 | 
114 |   const { unmount } = render(
115 |     <Switch>
116 |       <Route path="/foo/:id">
117 |         {(params) => {
118 |           if (!params)
119 |             throw new Error("Render prop is called with falsy params!");
120 |           return null;
121 |         }}
122 |       </Route>
123 |     </Switch>
124 |   );
125 | 
126 |   await act(async () => {
127 |     await raf();
128 |     history.pushState(null, "", "/");
129 |   });
130 | 
131 |   unmount();
132 | });
133 | 
134 | it("supports catch-all routes with wildcard segments", async () => {
135 |   const { container } = testRouteRender(
136 |     "/something-different",
137 |     <Switch>
138 |       <Route path="/users">
139 |         <h1 />
140 |       </Route>
141 |       <Route path="/:anything*">
142 |         <h2 />
143 |       </Route>
144 |     </Switch>
145 |   );
146 | 
147 |   // Should match the catch-all route
148 |   expect(container.querySelectorAll("h1, h2")).toHaveLength(1);
149 |   expect(container.querySelector("h2")).toBeInTheDocument();
150 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
151 | });
152 | 
153 | it("uses a route without a path prop as a fallback", async () => {
154 |   const { container } = testRouteRender(
155 |     "/something-different",
156 |     <Switch>
157 |       <Route path="/users">
158 |         <h1 />
159 |       </Route>
160 |       <Route>
161 |         <h2 />
162 |       </Route>
163 |     </Switch>
164 |   );
165 | 
166 |   // Should match the fallback route (no path)
167 |   expect(container.querySelectorAll("h1, h2")).toHaveLength(1);
168 |   expect(container.querySelector("h2")).toBeInTheDocument();
169 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
170 | });
171 | 
172 | it("correctly handles arrays as children", async () => {
173 |   const { container } = testRouteRender(
174 |     "/in-array-3",
175 |     <Switch>
176 |       {[1, 2, 3].map((i) => {
177 |         const H = `h${i}` as keyof JSX.IntrinsicElements;
178 |         return (
179 |           <Route key={i} path={"/in-array-" + i}>
180 |             <H />
181 |           </Route>
182 |         );
183 |       })}
184 |       <Route>
185 |         <h4 />
186 |       </Route>
187 |     </Switch>
188 |   );
189 | 
190 |   // Should match the third route (/in-array-3)
191 |   expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1);
192 |   expect(container.querySelector("h3")).toBeInTheDocument();
193 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
194 |   expect(container.querySelector("h2")).not.toBeInTheDocument();
195 |   expect(container.querySelector("h4")).not.toBeInTheDocument();
196 | });
197 | 
198 | it("correctly handles fragments as children", async () => {
199 |   const { container } = testRouteRender(
200 |     "/in-fragment-2",
201 |     <Switch>
202 |       <>
203 |         {[1, 2, 3].map((i) => {
204 |           const H = `h${i}` as keyof JSX.IntrinsicElements;
205 |           return (
206 |             <Route key={i} path={"/in-fragment-" + i}>
207 |               <H />
208 |             </Route>
209 |           );
210 |         })}
211 |       </>
212 |       <Route>
213 |         <h4 />
214 |       </Route>
215 |     </Switch>
216 |   );
217 | 
218 |   // Should match the second route (/in-fragment-2)
219 |   expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1);
220 |   expect(container.querySelector("h2")).toBeInTheDocument();
221 |   expect(container.querySelector("h1")).not.toBeInTheDocument();
222 |   expect(container.querySelector("h3")).not.toBeInTheDocument();
223 |   expect(container.querySelector("h4")).not.toBeInTheDocument();
224 | });
225 | 


--------------------------------------------------------------------------------
/packages/wouter/test/test-utils.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Executes a callback and returns a promise that resolve when `hashchange` event is fired.
 3 |  * Rejects after `throwAfter` milliseconds.
 4 |  */
 5 | export const waitForHashChangeEvent = async (
 6 |   cb: () => void,
 7 |   throwAfter = 1000
 8 | ) =>
 9 |   new Promise<void>((resolve, reject) => {
10 |     let timeout: ReturnType<typeof setTimeout>;
11 | 
12 |     const onChange = () => {
13 |       resolve();
14 |       clearTimeout(timeout);
15 |       window.removeEventListener("hashchange", onChange);
16 |     };
17 | 
18 |     window.addEventListener("hashchange", onChange);
19 |     cb();
20 | 
21 |     timeout = setTimeout(() => {
22 |       reject(new Error("Timed out: `hashchange` event did not fire!"));
23 |       window.removeEventListener("hashchange", onChange);
24 |     }, throwAfter);
25 |   });
26 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-browser-location.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, assertType, describe, expectTypeOf } from "vitest";
 2 | import {
 3 |   useBrowserLocation,
 4 |   useSearch,
 5 |   useHistoryState,
 6 | } from "wouter/use-browser-location";
 7 | 
 8 | describe("useBrowserLocation", () => {
 9 |   it("should return string, function tuple", () => {
10 |     const [loc, navigate] = useBrowserLocation();
11 | 
12 |     assertType<string>(loc);
13 |     assertType<Function>(navigate);
14 |   });
15 | 
16 |   it("should return `navigate` function with `path` and `options` parameters", () => {
17 |     const [, navigate] = useBrowserLocation();
18 | 
19 |     assertType(navigate("/path"));
20 |     assertType(navigate(""));
21 | 
22 |     // @ts-expect-error
23 |     assertType(navigate());
24 |     // @ts-expect-error
25 |     assertType(navigate(null));
26 | 
27 |     assertType(navigate("/path", { replace: true }));
28 |     // @ts-expect-error
29 |     assertType(navigate("/path", { unknownOption: true }));
30 |   });
31 | 
32 |   it("should support `ssrPath` option", () => {
33 |     assertType(useBrowserLocation({ ssrPath: "/something" }));
34 |     // @ts-expect-error
35 |     assertType(useBrowserLocation({ foo: "bar" }));
36 |   });
37 | });
38 | 
39 | describe("useSearch", () => {
40 |   it("should return string", () => {
41 |     type Search = ReturnType<typeof useSearch>;
42 |     const search = useSearch();
43 | 
44 |     assertType<string>(search);
45 |     const allowedSearchValues: Search[] = ["", "?leading", "no-?-sign"];
46 |   });
47 | });
48 | 
49 | describe("useHistoryState", () => {
50 |   it("should support generics", () => {
51 |     type TestCase = { hello: string };
52 |     const state = useHistoryState<TestCase>();
53 | 
54 |     expectTypeOf(state).toEqualTypeOf<TestCase>();
55 |   });
56 | 
57 |   it("should fallback to any when type doesn't provided", () => {
58 |     const state = useHistoryState();
59 | 
60 |     expectTypeOf(state).toEqualTypeOf<any>();
61 |   });
62 | });
63 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-browser-location.test.tsx:
--------------------------------------------------------------------------------
  1 | import { useEffect } from "react";
  2 | import { it, expect, describe, beforeEach } from "vitest";
  3 | import { renderHook, act, waitFor } from "@testing-library/react";
  4 | import {
  5 |   useBrowserLocation,
  6 |   navigate,
  7 |   useSearch,
  8 |   useHistoryState,
  9 | } from "wouter/use-browser-location";
 10 | 
 11 | it("returns a pair [value, update]", () => {
 12 |   const { result, unmount } = renderHook(() => useBrowserLocation());
 13 |   const [value, update] = result.current;
 14 | 
 15 |   expect(typeof value).toBe("string");
 16 |   expect(typeof update).toBe("function");
 17 |   unmount();
 18 | });
 19 | 
 20 | describe("`value` first argument", () => {
 21 |   beforeEach(() => history.replaceState(null, "", "/"));
 22 | 
 23 |   it("reflects the current pathname", () => {
 24 |     const { result, unmount } = renderHook(() => useBrowserLocation());
 25 |     expect(result.current[0]).toBe("/");
 26 |     unmount();
 27 |   });
 28 | 
 29 |   it("reacts to `pushState` / `replaceState`", () => {
 30 |     const { result, unmount } = renderHook(() => useBrowserLocation());
 31 | 
 32 |     act(() => history.pushState(null, "", "/foo"));
 33 |     expect(result.current[0]).toBe("/foo");
 34 | 
 35 |     act(() => history.replaceState(null, "", "/bar"));
 36 |     expect(result.current[0]).toBe("/bar");
 37 |     unmount();
 38 |   });
 39 | 
 40 |   it("supports history.back() navigation", async () => {
 41 |     const { result, unmount } = renderHook(() => useBrowserLocation());
 42 | 
 43 |     act(() => history.pushState(null, "", "/foo"));
 44 |     await waitFor(() => expect(result.current[0]).toBe("/foo"));
 45 | 
 46 |     act(() => {
 47 |       history.back();
 48 |     });
 49 | 
 50 |     // Workaround for happy-dom: manually dispatch popstate event
 51 |     // happy-dom doesn't fully implement history.back() popstate events
 52 |     act(() => {
 53 |       const popstateEvent = new PopStateEvent("popstate", {
 54 |         state: history.state,
 55 |       });
 56 |       window.dispatchEvent(popstateEvent);
 57 |     });
 58 | 
 59 |     await waitFor(() => expect(result.current[0]).toBe("/"), { timeout: 1000 });
 60 |     unmount();
 61 |   });
 62 | 
 63 |   it("supports history state", () => {
 64 |     const { result, unmount } = renderHook(() => useBrowserLocation());
 65 |     const { result: state, unmount: unmountState } = renderHook(() =>
 66 |       useHistoryState()
 67 |     );
 68 | 
 69 |     const navigate = result.current[1];
 70 | 
 71 |     act(() => navigate("/path", { state: { hello: "world" } }));
 72 | 
 73 |     expect(state.current).toStrictEqual({ hello: "world" });
 74 | 
 75 |     unmount();
 76 |     unmountState();
 77 |   });
 78 | 
 79 |   it("uses fail-safe escaping", () => {
 80 |     const { result } = renderHook(() => useBrowserLocation());
 81 |     const navigate = result.current[1];
 82 | 
 83 |     act(() => navigate("/%not-valid"));
 84 |     expect(result.current[0]).toBe("/%not-valid");
 85 | 
 86 |     act(() => navigate("/99%"));
 87 |     expect(result.current[0]).toBe("/99%");
 88 |   });
 89 | });
 90 | 
 91 | describe("`useSearch` hook", () => {
 92 |   beforeEach(() => history.replaceState(null, "", "/"));
 93 | 
 94 |   it("allows to get current search string", () => {
 95 |     const { result: searchResult } = renderHook(() => useSearch());
 96 |     act(() => navigate("/foo?hello=world&whats=up"));
 97 | 
 98 |     expect(searchResult.current).toBe("?hello=world&whats=up");
 99 |   });
100 | 
101 |   it("returns empty string when there is no search string", () => {
102 |     const { result: searchResult } = renderHook(() => useSearch());
103 | 
104 |     expect(searchResult.current).toBe("");
105 | 
106 |     act(() => navigate("/foo"));
107 |     expect(searchResult.current).toBe("");
108 | 
109 |     act(() => navigate("/foo? "));
110 |     expect(searchResult.current).toBe("");
111 |   });
112 | 
113 |   it("does not re-render when only pathname is changed", () => {
114 |     // count how many times each hook is rendered
115 |     const locationRenders = { current: 0 };
116 |     const searchRenders = { current: 0 };
117 | 
118 |     // count number of rerenders for each hook
119 |     renderHook(() => {
120 |       useEffect(() => {
121 |         locationRenders.current += 1;
122 |       });
123 |       return useBrowserLocation();
124 |     });
125 | 
126 |     renderHook(() => {
127 |       useEffect(() => {
128 |         searchRenders.current += 1;
129 |       });
130 |       return useSearch();
131 |     });
132 | 
133 |     expect(locationRenders.current).toBe(1);
134 |     expect(searchRenders.current).toBe(1);
135 | 
136 |     act(() => navigate("/foo"));
137 | 
138 |     expect(locationRenders.current).toBe(2);
139 |     expect(searchRenders.current).toBe(1);
140 | 
141 |     act(() => navigate("/foo?bar"));
142 |     expect(locationRenders.current).toBe(2); // no re-render
143 |     expect(searchRenders.current).toBe(2);
144 | 
145 |     act(() => navigate("/baz?bar"));
146 |     expect(locationRenders.current).toBe(3); // no re-render
147 |     expect(searchRenders.current).toBe(2);
148 |   });
149 | });
150 | 
151 | describe("`update` second parameter", () => {
152 |   it("rerenders the component", () => {
153 |     const { result, unmount } = renderHook(() => useBrowserLocation());
154 |     const update = result.current[1];
155 | 
156 |     act(() => update("/about"));
157 |     expect(result.current[0]).toBe("/about");
158 |     unmount();
159 |   });
160 | 
161 |   it("changes the current location", () => {
162 |     const { result, unmount } = renderHook(() => useBrowserLocation());
163 |     const update = result.current[1];
164 | 
165 |     act(() => update("/about"));
166 |     expect(location.pathname).toBe("/about");
167 |     unmount();
168 |   });
169 | 
170 |   it("saves a new entry in the History object", () => {
171 |     const { result, unmount } = renderHook(() => useBrowserLocation());
172 |     const update = result.current[1];
173 | 
174 |     const histBefore = history.length;
175 |     act(() => update("/about"));
176 | 
177 |     expect(history.length).toBe(histBefore + 1);
178 |     unmount();
179 |   });
180 | 
181 |   it("replaces last entry with a new entry in the History object", () => {
182 |     const { result, unmount } = renderHook(() => useBrowserLocation());
183 |     const update = result.current[1];
184 | 
185 |     const histBefore = history.length;
186 |     act(() => update("/foo", { replace: true }));
187 | 
188 |     expect(history.length).toBe(histBefore);
189 |     expect(location.pathname).toBe("/foo");
190 |     unmount();
191 |   });
192 | 
193 |   it("stays the same reference between re-renders (function ref)", () => {
194 |     const { result, rerender, unmount } = renderHook(() =>
195 |       useBrowserLocation()
196 |     );
197 | 
198 |     const updateWas = result.current[1];
199 |     rerender();
200 |     const updateNow = result.current[1];
201 | 
202 |     expect(updateWas).toBe(updateNow);
203 |     unmount();
204 |   });
205 | });
206 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-hash-location.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, assertType, describe, expectTypeOf } from "vitest";
 2 | import { useHashLocation, navigate } from "wouter/use-hash-location";
 3 | import { BaseLocationHook } from "wouter";
 4 | 
 5 | it("is a location hook", () => {
 6 |   expectTypeOf(useHashLocation).toMatchTypeOf<BaseLocationHook>();
 7 |   expectTypeOf(useHashLocation()).toMatchTypeOf<[string, Function]>();
 8 | });
 9 | 
10 | it("accepts a `ssrPath` path option", () => {
11 |   useHashLocation({ ssrPath: "/foo" });
12 |   useHashLocation({ ssrPath: "" });
13 | 
14 |   // @ts-expect-error
15 |   useHashLocation({ base: 123 });
16 |   // @ts-expect-error
17 |   useHashLocation({ unknown: "/base" });
18 | });
19 | 
20 | describe("`navigate` function", () => {
21 |   it("accepts an arbitrary `state` option", () => {
22 |     navigate("/object", { state: { foo: "bar" } });
23 |     navigate("/symbol", { state: Symbol("foo") });
24 |     navigate("/string", { state: "foo" });
25 |     navigate("/undef", { state: undefined });
26 |   });
27 | 
28 |   it("returns nothing", () => {
29 |     assertType<void>(navigate("/foo"));
30 |   });
31 | });
32 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-hash-location.test.tsx:
--------------------------------------------------------------------------------
  1 | import { it, expect, beforeEach, vi } from "vitest";
  2 | import { renderHook, render } from "@testing-library/react";
  3 | import { renderToStaticMarkup } from "react-dom/server";
  4 | 
  5 | import { Router, Route, useLocation, Link } from "wouter";
  6 | import { useHashLocation } from "wouter/use-hash-location";
  7 | 
  8 | import { waitForHashChangeEvent } from "./test-utils";
  9 | import { ReactNode, useSyncExternalStore } from "react";
 10 | 
 11 | beforeEach(() => {
 12 |   history.replaceState(null, "", "/");
 13 |   location.hash = "";
 14 | });
 15 | 
 16 | it("gets current location from `location.hash`", () => {
 17 |   location.hash = "/app/users";
 18 |   const { result } = renderHook(() => useHashLocation());
 19 |   const [path] = result.current;
 20 | 
 21 |   expect(path).toBe("/app/users");
 22 | });
 23 | 
 24 | it("isn't sensitive to leading slash", () => {
 25 |   location.hash = "app/users";
 26 |   const { result } = renderHook(() => useHashLocation());
 27 |   const [path] = result.current;
 28 | 
 29 |   expect(path).toBe("/app/users");
 30 | });
 31 | 
 32 | it("rerenders when hash changes", async () => {
 33 |   const { result } = renderHook(() => useHashLocation());
 34 | 
 35 |   expect(result.current[0]).toBe("/");
 36 | 
 37 |   await waitForHashChangeEvent(() => {
 38 |     location.hash = "/app/users";
 39 |   });
 40 | 
 41 |   expect(result.current[0]).toBe("/app/users");
 42 | });
 43 | 
 44 | it("changes current hash when navigation is performed", () => {
 45 |   const { result } = renderHook(() => useHashLocation());
 46 |   const [, navigate] = result.current;
 47 | 
 48 |   navigate("/app/users");
 49 |   expect(location.hash).toBe("#/app/users");
 50 | });
 51 | 
 52 | it("should not rerender when pathname changes", () => {
 53 |   let renderCount = 0;
 54 |   location.hash = "/app";
 55 | 
 56 |   const { result } = renderHook(() => {
 57 |     useHashLocation();
 58 |     return ++renderCount;
 59 |   });
 60 | 
 61 |   expect(result.current).toBe(1);
 62 |   history.replaceState(null, "", "/foo?bar#/app");
 63 | 
 64 |   expect(result.current).toBe(1);
 65 | });
 66 | 
 67 | it("does not change anything besides the hash when doesn't contain ? symbol", () => {
 68 |   history.replaceState(null, "", "/foo?bar#/app");
 69 | 
 70 |   const { result } = renderHook(() => useHashLocation());
 71 |   const [, navigate] = result.current;
 72 | 
 73 |   navigate("/settings/general");
 74 |   expect(location.pathname).toBe("/foo");
 75 |   expect(location.search).toBe("?bar");
 76 | });
 77 | 
 78 | it("changes search and hash when contains ? symbol", () => {
 79 |   history.replaceState(null, "", "/foo?bar#/app");
 80 | 
 81 |   const { result } = renderHook(() => useHashLocation());
 82 |   const [, navigate] = result.current;
 83 | 
 84 |   navigate("/abc?def");
 85 |   expect(location.pathname).toBe("/foo");
 86 |   expect(location.search).toBe("?def");
 87 |   expect(location.hash).toBe("#/abc");
 88 | });
 89 | 
 90 | it("creates a new history entry when navigating", () => {
 91 |   const { result } = renderHook(() => useHashLocation());
 92 |   const [, navigate] = result.current;
 93 | 
 94 |   const initialLength = history.length;
 95 |   navigate("/about");
 96 |   expect(history.length).toBe(initialLength + 1);
 97 | });
 98 | 
 99 | it("supports `state` option when navigating", () => {
100 |   const { result } = renderHook(() => useHashLocation());
101 |   const [, navigate] = result.current;
102 | 
103 |   navigate("/app/users", { state: { hello: "world" } });
104 |   expect(history.state).toStrictEqual({ hello: "world" });
105 | });
106 | 
107 | it("never changes reference to `navigate` between rerenders", () => {
108 |   const { result, rerender } = renderHook(() => useHashLocation());
109 | 
110 |   const updateWas = result.current[1];
111 |   rerender();
112 | 
113 |   expect(result.current[1]).toBe(updateWas);
114 | });
115 | 
116 | it("uses `ssrPath` when rendered on the server", () => {
117 |   const App = () => {
118 |     const [path] = useHashLocation({ ssrPath: "/hello-from-server" });
119 |     return <>{path}</>;
120 |   };
121 | 
122 |   const rendered = renderToStaticMarkup(<App />);
123 |   expect(rendered).toBe("/hello-from-server");
124 | });
125 | 
126 | it("is not sensitive to leading / or # when navigating", async () => {
127 |   const { result } = renderHook(() => useHashLocation());
128 |   const [, navigate] = result.current;
129 | 
130 |   await waitForHashChangeEvent(() => navigate("look-ma-no-slashes"));
131 |   expect(location.hash).toBe("#/look-ma-no-slashes");
132 |   expect(result.current[0]).toBe("/look-ma-no-slashes");
133 | 
134 |   await waitForHashChangeEvent(() => navigate("#/look-ma-no-hashes"));
135 |   expect(location.hash).toBe("#/look-ma-no-hashes");
136 |   expect(result.current[0]).toBe("/look-ma-no-hashes");
137 | });
138 | 
139 | it("works even if `hashchange` listeners are called asynchronously ", async () => {
140 |   const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
141 | 
142 |   // we want `hashchange` to stop invoking listeners before it reaches the
143 |   // outer <Route path="/a" />. this is done to simulate a situation when
144 |   // `hashchange` listeners are called asynchrounously
145 |   //
146 |   // per https://github.com/whatwg/html/issues/1792
147 |   // some browsers fire `hashchange` and `popstate` asynchronously, so
148 |   // when the event listeners are called, a microtask can be scheduled in between,
149 |   // and we may end up with a teared state. inner components subscribe to `hashchange`
150 |   // earlier so they may render even though their parent route does not match
151 |   const subscribeToHashchange = (cb: () => void) => {
152 |     const fn = (event: HashChangeEvent) => {
153 |       event.stopImmediatePropagation();
154 |       cb();
155 |     };
156 | 
157 |     window.addEventListener("hashchange", fn);
158 |     return () => window.removeEventListener("hashchange", fn);
159 |   };
160 | 
161 |   const InterceptAndStopHashchange = ({
162 |     children,
163 |   }: {
164 |     children: ReactNode;
165 |   }) => {
166 |     useSyncExternalStore(subscribeToHashchange, () => true);
167 |     return <>{children}</>;
168 |   };
169 | 
170 |   const paths: string[] = [];
171 | 
172 |   // keep track of rendered paths
173 |   const LogLocations = () => {
174 |     paths.push(useLocation()[0]);
175 |     return null;
176 |   };
177 | 
178 |   location.hash = "#/a";
179 | 
180 |   const { unmount } = render(
181 |     <Router hook={useHashLocation}>
182 |       <Route path="/a">
183 |         <InterceptAndStopHashchange>
184 |           <LogLocations />
185 |         </InterceptAndStopHashchange>
186 |       </Route>
187 |     </Router>
188 |   );
189 | 
190 |   location.hash = "#/b";
191 | 
192 |   // wait for all `hashchange` listeners to be called
193 |   // can't use `waitForHashChangeEvent` here because it gets cancelled along the way
194 |   await nextTick();
195 | 
196 |   // paths should not contain "b", because the outer route
197 |   // does not match, so inner component should not be rendered
198 |   expect(paths).toEqual(["/a"]);
199 |   unmount();
200 | });
201 | 
202 | it("defines a custom way of rendering link hrefs", () => {
203 |   const { getByTestId } = render(
204 |     <Router hook={useHashLocation}>
205 |       <Link href="/app" data-testid="link" />
206 |     </Router>
207 |   );
208 | 
209 |   expect(getByTestId("link")).toHaveAttribute("href", "#/app");
210 | });
211 | 
212 | it("interacts properly with the history stack", () => {
213 |   const { result } = renderHook(() => useHashLocation());
214 |   const [, navigate] = result.current;
215 | 
216 |   // case: replace, expect no history stack changes
217 |   const historyStackCountBeforeReplace = history.length;
218 |   navigate("/app/users", { replace: true });
219 |   expect(location.hash).toBe("#/app/users");
220 |   expect(history.length).toBe(historyStackCountBeforeReplace);
221 | 
222 |   // case: push, expect history stack increase by 1
223 |   const historyStackCountBeforePush = history.length;
224 |   navigate("/app/users/2");
225 |   expect(location.hash).toBe("#/app/users/2");
226 |   expect(history.length).toBe(historyStackCountBeforePush + 1);
227 | });
228 | 
229 | it("dispatches hashchange event when options.replace is true", () => {
230 |   const { result } = renderHook(() => useHashLocation());
231 |   const [, navigate] = result.current;
232 | 
233 |   const hashChangeFn = vi.fn();
234 |   addEventListener("hashchange", hashChangeFn);
235 | 
236 |   navigate("/foo/bar", { replace: true });
237 |   expect(hashChangeFn).toBeCalled();
238 | 
239 |   removeEventListener("hashchange", hashChangeFn);
240 | });
241 | 
242 | it("detects history change when navigate with options.replace is called", async () => {
243 |   const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
244 | 
245 |   const { result } = renderHook(() => useHashLocation());
246 |   const [, navigate] = result.current;
247 | 
248 |   const newPath = "/foo/bar/baz";
249 |   navigate(newPath, { replace: true });
250 |   await nextTick();
251 |   expect(result.current[0]).toBe(newPath);
252 | });
253 | 
254 | it("uses string URLs as hashchange event payload", () => {
255 |   const { result } = renderHook(() => useHashLocation());
256 |   const [, navigate] = result.current;
257 | 
258 |   const relativeOldPath = "/foo";
259 |   const relativeNewPath = "/foo/bar/#hash";
260 |   const baseURL = "http://localhost:3000/#";
261 | 
262 |   navigate(relativeOldPath);
263 | 
264 |   let changeEvent = new HashChangeEvent("hashchange");
265 |   const hashChangeFn = (event: HashChangeEvent) => {
266 |     changeEvent = event;
267 |   };
268 | 
269 |   addEventListener("hashchange", hashChangeFn);
270 | 
271 |   navigate(relativeNewPath);
272 |   expect(changeEvent?.newURL).toBe(`${baseURL}${relativeNewPath}`);
273 |   expect(changeEvent?.oldURL).toBe(`${baseURL}${relativeOldPath}`);
274 | 
275 |   removeEventListener("hashchange", hashChangeFn);
276 | });
277 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-location.test.tsx:
--------------------------------------------------------------------------------
  1 | import { ComponentProps, ReactNode } from "react";
  2 | import { it, expect, describe, beforeEach } from "vitest";
  3 | import { renderHook, act } from "@testing-library/react";
  4 | import { Router, useLocation } from "wouter";
  5 | import {
  6 |   useBrowserLocation,
  7 |   navigate as browserNavigation,
  8 | } from "wouter/use-browser-location";
  9 | 
 10 | import {
 11 |   useHashLocation,
 12 |   navigate as hashNavigation,
 13 | } from "wouter/use-hash-location";
 14 | import { waitForHashChangeEvent } from "./test-utils";
 15 | 
 16 | import { memoryLocation } from "wouter/memory-location";
 17 | 
 18 | function createContainer(
 19 |   options: Omit<ComponentProps<typeof Router>, "children"> = {}
 20 | ) {
 21 |   return ({ children }: { children: ReactNode }) => (
 22 |     <Router {...options}>{children}</Router>
 23 |   );
 24 | }
 25 | 
 26 | const memory = memoryLocation({ record: true });
 27 | 
 28 | describe.each([
 29 |   {
 30 |     name: "useBrowserLocation",
 31 |     hook: useBrowserLocation,
 32 |     location: () => location.pathname,
 33 |     navigate: browserNavigation,
 34 |     act,
 35 |     clear: () => {
 36 |       history.replaceState(null, "", "/");
 37 |     },
 38 |   },
 39 |   {
 40 |     name: "useHashLocation",
 41 |     hook: useHashLocation,
 42 |     location: () => "/" + location.hash.replace(/^#?\/?/, ""),
 43 |     navigate: hashNavigation,
 44 |     act: (cb: () => void) => waitForHashChangeEvent(() => act(cb)),
 45 |     clear: () => {
 46 |       location.hash = "";
 47 |       history.replaceState(null, "", "/");
 48 |     },
 49 |   },
 50 |   {
 51 |     name: "memoryLocation",
 52 |     hook: memory.hook,
 53 |     location: () => memory.history.at(-1) ?? "",
 54 |     navigate: memory.navigate,
 55 |     act,
 56 |     clear: () => {
 57 |       memory.reset();
 58 |     },
 59 |   },
 60 | ])("$name", (stub) => {
 61 |   beforeEach(() => stub.clear());
 62 | 
 63 |   it("returns a pair [value, update]", () => {
 64 |     const { result, unmount } = renderHook(() => useLocation(), {
 65 |       wrapper: createContainer({ hook: stub.hook }),
 66 |     });
 67 |     const [value, update] = result.current;
 68 | 
 69 |     expect(typeof value).toBe("string");
 70 |     expect(typeof update).toBe("function");
 71 |     unmount();
 72 |   });
 73 | 
 74 |   describe("`value` first argument", () => {
 75 |     it("returns `/` when URL contains only a basepath", async () => {
 76 |       const { result, unmount } = renderHook(() => useLocation(), {
 77 |         wrapper: createContainer({
 78 |           base: "/app",
 79 |           hook: stub.hook,
 80 |         }),
 81 |       });
 82 | 
 83 |       await stub.act(() => stub.navigate("/app"));
 84 |       expect(result.current[0]).toBe("/");
 85 |       unmount();
 86 |     });
 87 | 
 88 |     it("basepath should be case-insensitive", async () => {
 89 |       const { result, unmount } = renderHook(() => useLocation(), {
 90 |         wrapper: createContainer({
 91 |           base: "/MyApp",
 92 |           hook: stub.hook,
 93 |         }),
 94 |       });
 95 | 
 96 |       await stub.act(() => stub.navigate("/myAPP/users/JohnDoe"));
 97 |       expect(result.current[0]).toBe("/users/JohnDoe");
 98 |       unmount();
 99 |     });
100 | 
101 |     it("returns an absolute path in case of unmatched base path", async () => {
102 |       const { result, unmount } = renderHook(() => useLocation(), {
103 |         wrapper: createContainer({
104 |           base: "/MyApp",
105 |           hook: stub.hook,
106 |         }),
107 |       });
108 | 
109 |       await stub.act(() => stub.navigate("/MyOtherApp/users/JohnDoe"));
110 |       expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe");
111 |       unmount();
112 |     });
113 | 
114 |     it("automatically unescapes specials characters", async () => {
115 |       const { result, unmount } = renderHook(() => useLocation(), {
116 |         wrapper: createContainer({
117 |           hook: stub.hook,
118 |         }),
119 |       });
120 | 
121 |       await stub.act(() =>
122 |         stub.navigate("/пользователи/показать все/101/げんきです")
123 |       );
124 |       expect(result.current[0]).toBe(
125 |         "/пользователи/показать все/101/げんきです"
126 |       );
127 | 
128 |       await stub.act(() => stub.navigate("/%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"));
129 |       expect(result.current[0]).toBe("/шеллы");
130 |       unmount();
131 |     });
132 | 
133 |     it("can accept unescaped basepaths", async () => {
134 |       const { result, unmount } = renderHook(() => useLocation(), {
135 |         wrapper: createContainer({
136 |           base: "/hello мир", // basepath is not escaped
137 |           hook: stub.hook,
138 |         }),
139 |       });
140 | 
141 |       await stub.act(() => stub.navigate("/hello%20%D0%BC%D0%B8%D1%80/rel"));
142 |       expect(result.current[0]).toBe("/rel");
143 | 
144 |       unmount();
145 |     });
146 | 
147 |     it("can accept unescaped basepaths", async () => {
148 |       const { result, unmount } = renderHook(() => useLocation(), {
149 |         wrapper: createContainer({
150 |           base: "/hello%20%D0%BC%D0%B8%D1%80", // basepath is already escaped
151 |           hook: stub.hook,
152 |         }),
153 |       });
154 | 
155 |       await stub.act(() => stub.navigate("/hello мир/rel"));
156 |       expect(result.current[0]).toBe("/rel");
157 | 
158 |       unmount();
159 |     });
160 |   });
161 | 
162 |   describe("`update` second parameter", () => {
163 |     it("rerenders the component", async () => {
164 |       const { result, unmount } = renderHook(() => useLocation(), {
165 |         wrapper: createContainer({ hook: stub.hook }),
166 |       });
167 |       const update = result.current[1];
168 | 
169 |       await stub.act(() => update("/about"));
170 |       expect(stub.location()).toBe("/about");
171 |       unmount();
172 |     });
173 | 
174 |     it("stays the same reference between re-renders (function ref)", () => {
175 |       const { result, rerender, unmount } = renderHook(() => useLocation(), {
176 |         wrapper: createContainer({ hook: stub.hook }),
177 |       });
178 | 
179 |       const updateWas = result.current[1];
180 |       rerender();
181 |       const updateNow = result.current[1];
182 | 
183 |       expect(updateWas).toBe(updateNow);
184 |       unmount();
185 |     });
186 | 
187 |     it("supports a basepath", async () => {
188 |       const { result, unmount } = renderHook(() => useLocation(), {
189 |         wrapper: createContainer({
190 |           base: "/app",
191 |           hook: stub.hook,
192 |         }),
193 |       });
194 | 
195 |       const update = result.current[1];
196 | 
197 |       await stub.act(() => update("/dashboard"));
198 |       expect(stub.location()).toBe("/app/dashboard");
199 |       unmount();
200 |     });
201 | 
202 |     it("ignores the '/' basepath", async () => {
203 |       const { result, unmount } = renderHook(() => useLocation(), {
204 |         wrapper: createContainer({
205 |           base: "/",
206 |           hook: stub.hook,
207 |         }),
208 |       });
209 | 
210 |       const update = result.current[1];
211 | 
212 |       await stub.act(() => update("/dashboard"));
213 |       expect(stub.location()).toBe("/dashboard");
214 |       unmount();
215 |     });
216 |   });
217 | });
218 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-params.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, expectTypeOf } from "vitest";
 2 | import { useParams } from "wouter";
 3 | 
 4 | it("does not accept any arguments", () => {
 5 |   expectTypeOf<typeof useParams>().parameters.toEqualTypeOf<[]>();
 6 | });
 7 | 
 8 | it("returns an object with arbitrary parameters", () => {
 9 |   const params = useParams();
10 | 
11 |   expectTypeOf(params).toBeObject();
12 |   expectTypeOf(params.any).toEqualTypeOf<string | undefined>();
13 |   expectTypeOf(params[0]).toEqualTypeOf<string | undefined>();
14 | });
15 | 
16 | it("can infer the type of parameters from the route path", () => {
17 |   const params = useParams<"/app/users/:name?/:id">();
18 | 
19 |   expectTypeOf(params).toMatchTypeOf<{
20 |     0?: string;
21 |     1?: string;
22 |     id: string;
23 |     name?: string;
24 |   }>();
25 | });
26 | 
27 | it("can accept the custom type of parameters as a generic argument", () => {
28 |   const params = useParams<{ foo: number; bar?: string }>();
29 | 
30 |   expectTypeOf(params).toMatchTypeOf<{
31 |     foo: number;
32 |     bar?: string;
33 |   }>();
34 | 
35 |   //@ts-expect-error
36 |   return params.notFound;
37 | });
38 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-params.test.tsx:
--------------------------------------------------------------------------------
  1 | import { act, renderHook } from "@testing-library/react";
  2 | import { it, expect } from "vitest";
  3 | import { useParams, Router, Route, Switch } from "wouter";
  4 | 
  5 | import { memoryLocation } from "wouter/memory-location";
  6 | 
  7 | it("returns empty object when used outside of <Route />", () => {
  8 |   const { result } = renderHook(() => useParams());
  9 |   expect(result.current).toEqual({});
 10 | });
 11 | 
 12 | it("contains a * parameter when used inside an empty <Route />", () => {
 13 |   const { result } = renderHook(() => useParams(), {
 14 |     wrapper: (props) => (
 15 |       <Router hook={memoryLocation({ path: "/app-2/goods/tees" }).hook}>
 16 |         <Route>{props.children}</Route>
 17 |       </Router>
 18 |     ),
 19 |   });
 20 | 
 21 |   expect(result.current).toEqual({
 22 |     0: "app-2/goods/tees",
 23 |     "*": "app-2/goods/tees",
 24 |   });
 25 | });
 26 | 
 27 | it("returns an empty object when there are no params", () => {
 28 |   const { result } = renderHook(() => useParams(), {
 29 |     wrapper: (props) => <Route path="/">{props.children}</Route>,
 30 |   });
 31 | 
 32 |   expect(result.current).toEqual({});
 33 | });
 34 | 
 35 | it("contains parameters from the closest parent <Route />", () => {
 36 |   const { result } = renderHook(() => useParams(), {
 37 |     wrapper: (props) => (
 38 |       <Router hook={memoryLocation({ path: "/app/users/1/maria" }).hook}>
 39 |         <Route path="/app/:foo/*">
 40 |           <Route path="/app/users/:id/:name">{props.children}</Route>
 41 |         </Route>
 42 |       </Router>
 43 |     ),
 44 |   });
 45 | 
 46 |   expect(result.current).toMatchObject({
 47 |     0: "1",
 48 |     1: "maria",
 49 |     id: "1",
 50 |     name: "maria",
 51 |   });
 52 | });
 53 | 
 54 | it("inherits parameters from parent nested routes", () => {
 55 |   const { result } = renderHook(() => useParams(), {
 56 |     wrapper: (props) => (
 57 |       <Router
 58 |         hook={
 59 |           memoryLocation({ path: "/dash/users/10/alex/bio/john/summary-1" })
 60 |             .hook
 61 |         }
 62 |       >
 63 |         <Route path="/:page" nest>
 64 |           <Route path="/users/:id/:name" nest>
 65 |             <Route path="/bio/:name/*">{props.children}</Route>
 66 |           </Route>
 67 |         </Route>
 68 |       </Router>
 69 |     ),
 70 |   });
 71 | 
 72 |   expect(result.current).toMatchObject({
 73 |     name: "john", // name gets overriden
 74 |     "*": "summary-1",
 75 |     page: "dash",
 76 |     id: "10",
 77 |     // number params are overriden
 78 |     0: "john",
 79 |     1: "summary-1",
 80 |   });
 81 | });
 82 | 
 83 | it("rerenders with parameters change", () => {
 84 |   const { hook, navigate } = memoryLocation({ path: "/" });
 85 | 
 86 |   const { result } = renderHook(() => useParams(), {
 87 |     wrapper: (props) => (
 88 |       <Router hook={hook}>
 89 |         <Route path="/:a/:b">{props.children}</Route>
 90 |       </Router>
 91 |     ),
 92 |   });
 93 | 
 94 |   expect(result.current).toBeNull();
 95 | 
 96 |   act(() => navigate("/posts/all"));
 97 |   expect(result.current).toMatchObject({
 98 |     0: "posts",
 99 |     1: "all",
100 |     a: "posts",
101 |     b: "all",
102 |   });
103 | 
104 |   act(() => navigate("/posts/latest"));
105 |   expect(result.current).toMatchObject({
106 |     0: "posts",
107 |     1: "latest",
108 |     a: "posts",
109 |     b: "latest",
110 |   });
111 | });
112 | 
113 | it("extracts parameters of the nested route", () => {
114 |   const { hook } = memoryLocation({
115 |     path: "/v2/eth/txns",
116 |     static: true,
117 |   });
118 | 
119 |   const { result } = renderHook(() => useParams(), {
120 |     wrapper: (props) => (
121 |       <Router hook={hook}>
122 |         <Route path="/:version/:chain?" nest>
123 |           {props.children}
124 |         </Route>
125 |       </Router>
126 |     ),
127 |   });
128 | 
129 |   expect(result.current).toEqual({
130 |     0: "v2",
131 |     1: "eth",
132 |     version: "v2",
133 |     chain: "eth",
134 |   });
135 | });
136 | 
137 | it("keeps the object ref the same if params haven't changed", () => {
138 |   const { hook } = memoryLocation({ path: "/foo/bar" });
139 | 
140 |   const { result, rerender } = renderHook(() => useParams(), {
141 |     wrapper: (props) => (
142 |       <Router hook={hook}>
143 |         <Route path="/:a/:b/*?">{props.children}</Route>
144 |       </Router>
145 |     ),
146 |   });
147 | 
148 |   const firstRenderedParams = result.current;
149 |   rerender();
150 |   expect(result.current).toBe(firstRenderedParams);
151 | });
152 | 
153 | it("works when the route becomes matching", () => {
154 |   const { hook, navigate } = memoryLocation({ path: "/" });
155 | 
156 |   const { result } = renderHook(() => useParams(), {
157 |     wrapper: (props) => (
158 |       <Router hook={hook}>
159 |         <Route path="/:id">{props.children}</Route>
160 |       </Router>
161 |     ),
162 |   });
163 | 
164 |   act(() => navigate("/123"));
165 |   expect(result.current).toMatchObject({ id: "123" });
166 | });
167 | 
168 | it("makes the params an empty object, when there are no path params", () => {
169 |   const { hook, navigate } = memoryLocation({ path: "/" });
170 | 
171 |   const { result } = renderHook(() => useParams(), {
172 |     wrapper: (props) => (
173 |       <Router hook={hook}>
174 |         <Switch>
175 |           <Route path="/posts">{props.children}</Route>
176 |           <Route path="/posts/:a">{props.children}</Route>
177 |         </Switch>
178 |       </Router>
179 |     ),
180 |   });
181 | 
182 |   act(() => navigate("/posts/all"));
183 |   act(() => navigate("/posts"));
184 |   expect(Object.keys(result.current).length).toBe(0);
185 | });
186 | 
187 | it("removes route parameters when no longer present in the path", () => {
188 |   // Start at a route that has both 'category' and 'page' in its params
189 |   const { hook, navigate } = memoryLocation({
190 |     path: "/products/categories/apple/page/1",
191 |   });
192 | 
193 |   // Render useParams within two routes: one with /page/:page, one without
194 |   const { result } = renderHook(() => useParams(), {
195 |     wrapper: (props) => (
196 |       <Router hook={hook}>
197 |         <Switch>
198 |           <Route path="/products/categories/:category">{props.children}</Route>
199 |           <Route path="/products/categories/:category/page/:page">
200 |             {props.children}
201 |           </Route>
202 |         </Switch>
203 |       </Router>
204 |     ),
205 |   });
206 | 
207 |   // Initial params should include 'category' and 'page'
208 |   expect(result.current).toMatchObject({
209 |     0: "apple",
210 |     1: "1",
211 |     category: "apple",
212 |     page: "1",
213 |   });
214 | 
215 |   // Navigate to a path that no longer contains the page param
216 |   act(() => navigate("/products/categories/apple"));
217 | 
218 |   // The 'page' param should now be removed
219 |   expect(result.current).toEqual({
220 |     0: "apple",
221 |     category: "apple",
222 |   });
223 | });
224 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-route.test-d.ts:
--------------------------------------------------------------------------------
 1 | import { it, expectTypeOf, assertType } from "vitest";
 2 | import { useRoute } from "wouter";
 3 | 
 4 | it("should only accept strings", () => {
 5 |   // @ts-expect-error
 6 |   assertType(useRoute(Symbol()));
 7 |   // @ts-expect-error
 8 |   assertType(useRoute());
 9 |   assertType(useRoute("/"));
10 | });
11 | 
12 | it('has a boolean "match" result as a first returned value', () => {
13 |   const [match] = useRoute("/");
14 |   expectTypeOf(match).toEqualTypeOf<boolean>();
15 | });
16 | 
17 | it("returns null as parameters when there was no match", () => {
18 |   const [match, params] = useRoute("/foo");
19 | 
20 |   if (!match) {
21 |     expectTypeOf(params).toEqualTypeOf<null>();
22 |   }
23 | });
24 | 
25 | it("accepts the type of parameters as a generic argument", () => {
26 |   const [match, params] = useRoute<{ id: string; name: string | undefined }>(
27 |     "/app/users/:name?/:id"
28 |   );
29 | 
30 |   if (match) {
31 |     expectTypeOf(params).toEqualTypeOf<{
32 |       id: string;
33 |       name: string | undefined;
34 |     }>();
35 |   }
36 | });
37 | 
38 | it("infers parameters from the route path", () => {
39 |   const [, inferedParams] = useRoute("/app/users/:name?/:id/*?");
40 | 
41 |   if (inferedParams) {
42 |     expectTypeOf(inferedParams).toMatchTypeOf<{
43 |       0?: string;
44 |       1?: string;
45 |       2?: string;
46 |       name?: string;
47 |       id: string;
48 |       wildcard?: string;
49 |     }>();
50 |   }
51 | });
52 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-route.test.tsx:
--------------------------------------------------------------------------------
  1 | import { renderHook, act } from "@testing-library/react";
  2 | import { useRoute, Match, Router, RegexRouteParams } from "wouter";
  3 | import { it, expect } from "vitest";
  4 | import { memoryLocation } from "wouter/memory-location";
  5 | 
  6 | it("is case insensitive", () => {
  7 |   assertRoute("/Users", "/users", {});
  8 |   assertRoute("/HomePage", "/Homepage", {});
  9 |   assertRoute("/Users/:Name", "/users/alex", { 0: "alex", Name: "alex" });
 10 | });
 11 | 
 12 | it("supports required segments", () => {
 13 |   assertRoute("/:page", "/users", { 0: "users", page: "users" });
 14 |   assertRoute("/:page", "/users/all", false);
 15 |   assertRoute("/:page", "/1", { 0: "1", page: "1" });
 16 | 
 17 |   assertRoute("/home/:page/etc", "/home/users/etc", {
 18 |     0: "users",
 19 |     page: "users",
 20 |   });
 21 |   assertRoute("/home/:page/etc", "/home/etc", false);
 22 | 
 23 |   assertRoute(
 24 |     "/root/payments/:id/refunds/:refId",
 25 |     "/root/payments/1/refunds/2",
 26 |     [true, { 0: "1", 1: "2", id: "1", refId: "2" }]
 27 |   );
 28 | });
 29 | 
 30 | it("ignores the trailing slash", () => {
 31 |   assertRoute("/home", "/home/", {});
 32 |   assertRoute("/home", "/home", {});
 33 | 
 34 |   assertRoute("/home/", "/home/", {});
 35 |   assertRoute("/home/", "/home", {});
 36 | 
 37 |   assertRoute("/:page", "/users/", [true, { 0: "users", page: "users" }]);
 38 |   assertRoute("/catalog/:section?", "/catalog/", {
 39 |     0: undefined,
 40 |     section: undefined,
 41 |   });
 42 | });
 43 | 
 44 | it("supports trailing wildcards", () => {
 45 |   assertRoute("/app/*", "/app/", { 0: "", "*": "" });
 46 |   assertRoute("/app/*", "/app/dashboard/intro", {
 47 |     0: "dashboard/intro",
 48 |     "*": "dashboard/intro",
 49 |   });
 50 |   assertRoute("/app/*", "/app/charges/1", { 0: "charges/1", "*": "charges/1" });
 51 | });
 52 | 
 53 | it("supports wildcards in the middle of the pattern", () => {
 54 |   assertRoute("/app/*/settings", "/app/users/settings", {
 55 |     0: "users",
 56 |     "*": "users",
 57 |   });
 58 |   assertRoute("/app/*/settings", "/app/users/1/settings", {
 59 |     0: "users/1",
 60 |     "*": "users/1",
 61 |   });
 62 | 
 63 |   assertRoute("/*/payments/:id", "/home/payments/1", {
 64 |     0: "home",
 65 |     1: "1",
 66 |     "*": "home",
 67 |     id: "1",
 68 |   });
 69 |   assertRoute("/*/payments/:id?", "/home/payments", {
 70 |     0: "home",
 71 |     1: undefined,
 72 |     "*": "home",
 73 |     id: undefined,
 74 |   });
 75 | });
 76 | 
 77 | it("uses a question mark to define optional segments", () => {
 78 |   assertRoute("/books/:genre/:title?", "/books/scifi", {
 79 |     0: "scifi",
 80 |     1: undefined,
 81 |     genre: "scifi",
 82 |     title: undefined,
 83 |   });
 84 |   assertRoute("/books/:genre/:title?", "/books/scifi/dune", {
 85 |     0: "scifi",
 86 |     1: "dune",
 87 |     genre: "scifi",
 88 |     title: "dune",
 89 |   });
 90 |   assertRoute("/books/:genre/:title?", "/books/scifi/dune/all", false);
 91 | 
 92 |   assertRoute("/app/:company?/blog/:post", "/app/apple/blog/mac", {
 93 |     0: "apple",
 94 |     1: "mac",
 95 |     company: "apple",
 96 |     post: "mac",
 97 |   });
 98 | 
 99 |   assertRoute("/app/:company?/blog/:post", "/app/blog/mac", {
100 |     0: undefined,
101 |     1: "mac",
102 |     company: undefined,
103 |     post: "mac",
104 |   });
105 | });
106 | 
107 | it("supports optional wildcards", () => {
108 |   assertRoute("/app/*?", "/app/blog/mac", { 0: "blog/mac", "*": "blog/mac" });
109 |   assertRoute("/app/*?", "/app", { 0: undefined, "*": undefined });
110 |   assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { 0: "v1", "*": "v1" });
111 |   assertRoute("/app/*?/dashboard", "/app/dashboard", {
112 |     0: undefined,
113 |     "*": undefined,
114 |   });
115 |   assertRoute("/app/*?/users/:name", "/app/users/karen", {
116 |     0: undefined,
117 |     1: "karen",
118 |     "*": undefined,
119 |     name: "karen",
120 |   });
121 | });
122 | 
123 | it("supports other characters in segments", () => {
124 |   assertRoute("/users/:name", "/users/1-alex", { 0: "1-alex", name: "1-alex" });
125 |   assertRoute("/staff/:name/:bio?", "/staff/John Doe 3", {
126 |     0: "John Doe 3",
127 |     1: undefined,
128 |     name: "John Doe 3",
129 |     bio: undefined,
130 |   });
131 |   assertRoute("/staff/:name/:bio?", "/staff/John Doe 3/bio", {
132 |     0: "John Doe 3",
133 |     1: "bio",
134 |     name: "John Doe 3",
135 |     bio: "bio",
136 |   });
137 | 
138 |   assertRoute("/users/:name/bio", "/users/$102_Kathrine&/bio", {
139 |     0: "$102_Kathrine&",
140 |     name: "$102_Kathrine&",
141 |   });
142 | });
143 | 
144 | it("ignores escaped slashes", () => {
145 |   assertRoute("/:param/bar", "/foo%2Fbar/bar", {
146 |     0: "foo%2Fbar",
147 |     param: "foo%2Fbar",
148 |   });
149 |   assertRoute("/:param", "/foo%2Fbar%D1%81%D0%B0%D0%BD%D1%8F", {
150 |     0: "foo%2Fbarсаня",
151 |     param: "foo%2Fbarсаня",
152 |   });
153 | });
154 | 
155 | it("supports regex patterns", () => {
156 |   assertRoute(/[/]foo/, "/foo", {});
157 |   assertRoute(/[/]([a-z]+)/, "/bar", { 0: "bar" });
158 |   assertRoute(/[/]([a-z]+)/, "/123", false);
159 |   assertRoute(/[/](?<param>[a-z]+)/, "/bar", { 0: "bar", param: "bar" });
160 |   assertRoute(/[/](?<param>[a-z]+)/, "/123", false);
161 | });
162 | 
163 | it("reacts to pattern updates", () => {
164 |   const { result, rerender } = renderHook(
165 |     ({ pattern }: { pattern: string }) => useRoute(pattern),
166 |     {
167 |       wrapper: (props) => (
168 |         <Router
169 |           hook={
170 |             memoryLocation({ path: "/blog/products/40/read-all", static: true })
171 |               .hook
172 |           }
173 |           {...props}
174 |         />
175 |       ),
176 |       initialProps: { pattern: "/" },
177 |     }
178 |   );
179 | 
180 |   expect(result.current).toStrictEqual([false, null]);
181 | 
182 |   rerender({ pattern: "/blog/:category/:post/:action" });
183 |   expect(result.current).toStrictEqual([
184 |     true,
185 |     {
186 |       0: "products",
187 |       1: "40",
188 |       2: "read-all",
189 |       category: "products",
190 |       post: "40",
191 |       action: "read-all",
192 |     },
193 |   ]);
194 | 
195 |   rerender({ pattern: "/blog/products/:id?/read-all" });
196 |   expect(result.current).toStrictEqual([true, { 0: "40", id: "40" }]);
197 | 
198 |   rerender({ pattern: "/blog/products/:name" });
199 |   expect(result.current).toStrictEqual([false, null]);
200 | 
201 |   rerender({ pattern: "/blog/*" });
202 |   expect(result.current).toStrictEqual([
203 |     true,
204 |     { 0: "products/40/read-all", "*": "products/40/read-all" },
205 |   ]);
206 | });
207 | 
208 | it("reacts to location updates", () => {
209 |   const { hook, navigate } = memoryLocation();
210 | 
211 |   const { result } = renderHook(() => useRoute("/cities/:city?"), {
212 |     wrapper: (props) => <Router hook={hook} {...props} />,
213 |   });
214 | 
215 |   expect(result.current).toStrictEqual([false, null]);
216 | 
217 |   act(() => navigate("/cities/berlin"));
218 |   expect(result.current).toStrictEqual([true, { 0: "berlin", city: "berlin" }]);
219 | 
220 |   act(() => navigate("/cities/Tokyo"));
221 |   expect(result.current).toStrictEqual([true, { 0: "Tokyo", city: "Tokyo" }]);
222 | 
223 |   act(() => navigate("/about"));
224 |   expect(result.current).toStrictEqual([false, null]);
225 | 
226 |   act(() => navigate("/cities"));
227 |   expect(result.current).toStrictEqual([
228 |     true,
229 |     { 0: undefined, city: undefined },
230 |   ]);
231 | });
232 | 
233 | /**
234 |  * Assertion helper to test useRoute() return values.
235 |  */
236 | 
237 | const assertRoute = (
238 |   pattern: string | RegExp,
239 |   location: string,
240 |   rhs: false | Match | Record<string, string | undefined>
241 | ) => {
242 |   const { result } = renderHook(() => useRoute(pattern), {
243 |     wrapper: (props) => (
244 |       <Router
245 |         hook={memoryLocation({ path: location, static: true }).hook}
246 |         {...props}
247 |       />
248 |     ),
249 |   });
250 | 
251 |   if (rhs === false) {
252 |     expect(result.current).toStrictEqual([false, null]);
253 |   } else if (Array.isArray(rhs)) {
254 |     expect(result.current).toStrictEqual(rhs);
255 |   } else {
256 |     expect(result.current).toStrictEqual([true, rhs]);
257 |   }
258 | };
259 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-search-params.test.tsx:
--------------------------------------------------------------------------------
 1 | import { renderHook, act } from "@testing-library/react";
 2 | import { useSearchParams, Router } from "wouter";
 3 | import { navigate } from "wouter/use-browser-location";
 4 | import { it, expect, beforeEach } from "vitest";
 5 | 
 6 | beforeEach(() => history.replaceState(null, "", "/"));
 7 | 
 8 | it("can return browser search params", () => {
 9 |   history.replaceState(null, "", "/users?active=true");
10 |   const { result } = renderHook(() => useSearchParams());
11 | 
12 |   expect(result.current[0].get("active")).toBe("true");
13 | });
14 | 
15 | it("can change browser search params", () => {
16 |   history.replaceState(null, "", "/users?active=true");
17 |   const { result } = renderHook(() => useSearchParams());
18 | 
19 |   expect(result.current[0].get("active")).toBe("true");
20 | 
21 |   act(() =>
22 |     result.current[1]((prev) => {
23 |       prev.set("active", "false");
24 |       return prev;
25 |     })
26 |   );
27 | 
28 |   expect(result.current[0].get("active")).toBe("false");
29 | });
30 | 
31 | it("can be customized in the Router", () => {
32 |   const customSearchHook = ({ customOption = "unused" }) => "none";
33 | 
34 |   const { result } = renderHook(() => useSearchParams(), {
35 |     wrapper: (props) => {
36 |       return <Router searchHook={customSearchHook}>{props.children}</Router>;
37 |     },
38 |   });
39 | 
40 |   expect(Array.from(result.current[0].keys())).toEqual(["none"]);
41 | });
42 | 
43 | it("unescapes search string", () => {
44 |   const { result: searchResult } = renderHook(() => useSearchParams());
45 | 
46 |   expect(Array.from(searchResult.current[0].keys()).length).toBe(0);
47 | 
48 |   act(() => navigate("/?nonce=not Found&country=საქართველო"));
49 |   expect(searchResult.current[0].get("nonce")).toBe("not Found");
50 |   expect(searchResult.current[0].get("country")).toBe("საქართველო");
51 | 
52 |   // question marks
53 |   act(() => navigate("/?вопрос=как дела?"));
54 |   expect(searchResult.current[0].get("вопрос")).toBe("как дела?");
55 | });
56 | 
57 | it("is safe against parameter injection", () => {
58 |   history.replaceState(null, "", "/?search=foo%26parameter_injection%3Dbar");
59 |   const { result } = renderHook(() => useSearchParams());
60 | 
61 |   expect(result.current[0].get("search")).toBe("foo&parameter_injection=bar");
62 | });
63 | 


--------------------------------------------------------------------------------
/packages/wouter/test/use-search.test.tsx:
--------------------------------------------------------------------------------
 1 | import { renderHook, act } from "@testing-library/react";
 2 | import { useSearch, Router } from "wouter";
 3 | import { navigate } from "wouter/use-browser-location";
 4 | import { memoryLocation } from "wouter/memory-location";
 5 | import { it, expect, beforeEach } from "vitest";
 6 | 
 7 | beforeEach(() => history.replaceState(null, "", "/"));
 8 | 
 9 | it("returns browser search string", () => {
10 |   history.replaceState(null, "", "/users?active=true");
11 |   const { result } = renderHook(() => useSearch());
12 | 
13 |   expect(result.current).toEqual("active=true");
14 | });
15 | 
16 | it("can be customized in the Router", () => {
17 |   const customSearchHook = ({ customOption = "unused" }) => "none";
18 | 
19 |   const { result } = renderHook(() => useSearch(), {
20 |     wrapper: (props) => {
21 |       return <Router searchHook={customSearchHook}>{props.children}</Router>;
22 |     },
23 |   });
24 | 
25 |   expect(result.current).toEqual("none");
26 | });
27 | 
28 | it("can be customized with memoryLocation", () => {
29 |   const { searchHook } = memoryLocation({ path: "/foo?key=value" });
30 | 
31 |   const { result } = renderHook(() => useSearch(), {
32 |     wrapper: (props) => {
33 |       return <Router searchHook={searchHook}>{props.children}</Router>;
34 |     },
35 |   });
36 | 
37 |   expect(result.current).toEqual("key=value");
38 | });
39 | 
40 | it("can be customized with memoryLocation using search path parameter", () => {
41 |   const { searchHook } = memoryLocation({
42 |     path: "/foo?key=value",
43 |     searchPath: "foo=bar",
44 |   });
45 | 
46 |   const { result } = renderHook(() => useSearch(), {
47 |     wrapper: (props) => {
48 |       return <Router searchHook={searchHook}>{props.children}</Router>;
49 |     },
50 |   });
51 | 
52 |   expect(result.current).toEqual("key=value&foo=bar");
53 | });
54 | 
55 | it("unescapes search string", () => {
56 |   const { result: searchResult } = renderHook(() => useSearch());
57 | 
58 |   expect(searchResult.current).toBe("");
59 | 
60 |   act(() => navigate("/?nonce=not Found&country=საქართველო"));
61 |   expect(searchResult.current).toBe("nonce=not Found&country=საქართველო");
62 | 
63 |   // question marks
64 |   act(() => navigate("/?вопрос=как дела?"));
65 |   expect(searchResult.current).toBe("вопрос=как дела?");
66 | });
67 | 
68 | it("is safe against parameter injection", () => {
69 |   history.replaceState(null, "", "/?search=foo%26parameter_injection%3Dbar");
70 |   const { result } = renderHook(() => useSearch());
71 | 
72 |   const searchParams = new URLSearchParams(result.current);
73 |   const query = Object.fromEntries(searchParams.entries());
74 | 
75 |   expect(query).toEqual({ search: "foo&parameter_injection=bar" });
76 | });
77 | 


--------------------------------------------------------------------------------
/packages/wouter/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "moduleResolution": "node",
 5 |     "module": "es2020",
 6 |     "jsx": "react-jsx",
 7 |     "strict": true
 8 |   }
 9 | }
10 | 


--------------------------------------------------------------------------------
/packages/wouter/types/index.d.ts:
--------------------------------------------------------------------------------
  1 | // Minimum TypeScript Version: 4.1
  2 | 
  3 | // tslint:disable:no-unnecessary-generics
  4 | 
  5 | import {
  6 |   AnchorHTMLAttributes,
  7 |   FunctionComponent,
  8 |   RefAttributes,
  9 |   ComponentType,
 10 |   ReactNode,
 11 |   ReactElement,
 12 |   MouseEventHandler,
 13 | } from "react";
 14 | 
 15 | import {
 16 |   Path,
 17 |   PathPattern,
 18 |   BaseLocationHook,
 19 |   HookReturnValue,
 20 |   HookNavigationOptions,
 21 |   BaseSearchHook,
 22 | } from "./location-hook.js";
 23 | import {
 24 |   BrowserLocationHook,
 25 |   BrowserSearchHook,
 26 | } from "./use-browser-location.js";
 27 | 
 28 | import { Parser, RouterObject, RouterOptions } from "./router.js";
 29 | 
 30 | // these files only export types, so we can re-export them as-is
 31 | // in TS 5.0 we'll be able to use `export type * from ...`
 32 | export * from "./location-hook.js";
 33 | export * from "./router.js";
 34 | 
 35 | import { RouteParams } from "regexparam";
 36 | 
 37 | export type StringRouteParams<T extends string> = RouteParams<T> & {
 38 |   [param: number]: string | undefined;
 39 | };
 40 | export type RegexRouteParams = { [key: string | number]: string | undefined };
 41 | 
 42 | /**
 43 |  * Route patterns and parameters
 44 |  */
 45 | export interface DefaultParams {
 46 |   readonly [paramName: string | number]: string | undefined;
 47 | }
 48 | 
 49 | export type Params<T extends DefaultParams = DefaultParams> = T;
 50 | 
 51 | export type MatchWithParams<T extends DefaultParams = DefaultParams> = [
 52 |   true,
 53 |   Params<T>
 54 | ];
 55 | export type NoMatch = [false, null];
 56 | export type Match<T extends DefaultParams = DefaultParams> =
 57 |   | MatchWithParams<T>
 58 |   | NoMatch;
 59 | 
 60 | /*
 61 |  * Components: <Route />
 62 |  */
 63 | 
 64 | export interface RouteComponentProps<T extends DefaultParams = DefaultParams> {
 65 |   params: T;
 66 | }
 67 | 
 68 | export interface RouteProps<
 69 |   T extends DefaultParams | undefined = undefined,
 70 |   RoutePath extends PathPattern = PathPattern
 71 | > {
 72 |   children?:
 73 |     | ((
 74 |         params: T extends DefaultParams
 75 |           ? T
 76 |           : RoutePath extends string
 77 |           ? StringRouteParams<RoutePath>
 78 |           : RegexRouteParams
 79 |       ) => ReactNode)
 80 |     | ReactNode;
 81 |   path?: RoutePath;
 82 |   component?: ComponentType<
 83 |     RouteComponentProps<
 84 |       T extends DefaultParams
 85 |         ? T
 86 |         : RoutePath extends string
 87 |         ? StringRouteParams<RoutePath>
 88 |         : RegexRouteParams
 89 |     >
 90 |   >;
 91 |   nest?: boolean;
 92 | }
 93 | 
 94 | export function Route<
 95 |   T extends DefaultParams | undefined = undefined,
 96 |   RoutePath extends PathPattern = PathPattern
 97 | >(props: RouteProps<T, RoutePath>): ReturnType<FunctionComponent>;
 98 | 
 99 | /*
100 |  * Components: <Link /> & <Redirect />
101 |  */
102 | 
103 | export type NavigationalProps<
104 |   H extends BaseLocationHook = BrowserLocationHook
105 | > = ({ to: Path; href?: never } | { href: Path; to?: never }) &
106 |   HookNavigationOptions<H>;
107 | 
108 | export type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
109 |   NavigationalProps<H> & {
110 |     children?: never;
111 |   };
112 | 
113 | export function Redirect<H extends BaseLocationHook = BrowserLocationHook>(
114 |   props: RedirectProps<H>,
115 |   context?: any
116 | ): null;
117 | 
118 | type AsChildProps<ComponentProps, DefaultElementProps> =
119 |   | ({ asChild?: false } & DefaultElementProps)
120 |   | ({ asChild: true } & ComponentProps);
121 | 
122 | type HTMLLinkAttributes = Omit<
123 |   AnchorHTMLAttributes<HTMLAnchorElement>,
124 |   "className"
125 | > & {
126 |   className?: string | undefined | ((isActive: boolean) => string | undefined);
127 | };
128 | 
129 | export type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
130 |   NavigationalProps<H> &
131 |     AsChildProps<
132 |       { children: ReactElement; onClick?: MouseEventHandler },
133 |       HTMLLinkAttributes & RefAttributes<HTMLAnchorElement>
134 |     >;
135 | 
136 | export function Link<H extends BaseLocationHook = BrowserLocationHook>(
137 |   props: LinkProps<H>,
138 |   context?: any
139 | ): ReturnType<FunctionComponent>;
140 | 
141 | /*
142 |  * Components: <Switch />
143 |  */
144 | 
145 | export interface SwitchProps {
146 |   location?: string;
147 |   children: ReactNode;
148 | }
149 | export const Switch: FunctionComponent<SwitchProps>;
150 | 
151 | /*
152 |  * Components: <Router />
153 |  */
154 | 
155 | export type RouterProps = RouterOptions & {
156 |   children: ReactNode;
157 | };
158 | 
159 | export const Router: FunctionComponent<RouterProps>;
160 | 
161 | /*
162 |  * Hooks
163 |  */
164 | 
165 | export function useRouter(): RouterObject;
166 | 
167 | export function useRoute<
168 |   T extends DefaultParams | undefined = undefined,
169 |   RoutePath extends PathPattern = PathPattern
170 | >(
171 |   pattern: RoutePath
172 | ): Match<
173 |   T extends DefaultParams
174 |     ? T
175 |     : RoutePath extends string
176 |     ? StringRouteParams<RoutePath>
177 |     : RegexRouteParams
178 | >;
179 | 
180 | export function useLocation<
181 |   H extends BaseLocationHook = BrowserLocationHook
182 | >(): HookReturnValue<H>;
183 | 
184 | export function useSearch<
185 |   H extends BaseSearchHook = BrowserSearchHook
186 | >(): ReturnType<H>;
187 | 
188 | export type URLSearchParamsInit = ConstructorParameters<
189 |   typeof URLSearchParams
190 | >[0];
191 | export type SetSearchParams = (
192 |   nextInit:
193 |     | URLSearchParamsInit
194 |     | ((prev: URLSearchParams) => URLSearchParamsInit),
195 |   options?: { replace?: boolean; state?: any }
196 | ) => void;
197 | 
198 | export function useSearchParams(): [URLSearchParams, SetSearchParams];
199 | 
200 | export function useParams<T = undefined>(): T extends string
201 |   ? StringRouteParams<T>
202 |   : T extends undefined
203 |   ? DefaultParams
204 |   : T;
205 | 
206 | /*
207 |  * Helpers
208 |  */
209 | 
210 | export function matchRoute<
211 |   T extends DefaultParams | undefined = undefined,
212 |   RoutePath extends PathPattern = PathPattern
213 | >(
214 |   parser: Parser,
215 |   pattern: RoutePath,
216 |   path: string,
217 |   loose?: boolean
218 | ): Match<
219 |   T extends DefaultParams
220 |     ? T
221 |     : RoutePath extends string
222 |     ? StringRouteParams<RoutePath>
223 |     : RegexRouteParams
224 | >;
225 | 
226 | // tslint:enable:no-unnecessary-generics
227 | 


--------------------------------------------------------------------------------
/packages/wouter/types/location-hook.d.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Foundation: useLocation and paths
 3 |  */
 4 | 
 5 | export type Path = string;
 6 | 
 7 | export type PathPattern = string | RegExp;
 8 | 
 9 | export type SearchString = string;
10 | 
11 | // the base useLocation hook type. Any custom hook (including the
12 | // default one) should inherit from it.
13 | export type BaseLocationHook = (
14 |   ...args: any[]
15 | ) => [Path, (path: Path, ...args: any[]) => any];
16 | 
17 | export type BaseSearchHook = (...args: any[]) => SearchString;
18 | 
19 | /*
20 |  * Utility types that operate on hook
21 |  */
22 | 
23 | // Returns the type of the location tuple of the given hook.
24 | export type HookReturnValue<H extends BaseLocationHook> = ReturnType<H>;
25 | 
26 | // Utility type that allows us to handle cases like `any` and `never`
27 | type EmptyInterfaceWhenAnyOrNever<T> = 0 extends 1 & T
28 |   ? {}
29 |   : [T] extends [never]
30 |   ? {}
31 |   : T;
32 | 
33 | // Returns the type of the navigation options that hook's push function accepts.
34 | export type HookNavigationOptions<H extends BaseLocationHook> =
35 |   EmptyInterfaceWhenAnyOrNever<
36 |     NonNullable<Parameters<HookReturnValue<H>[1]>[1]> // get's the second argument of a tuple returned by the hook
37 |   >;
38 | 


--------------------------------------------------------------------------------
/packages/wouter/types/memory-location.d.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   BaseLocationHook,
 3 |   BaseSearchHook,
 4 |   Path,
 5 |   SearchString,
 6 | } from "./location-hook.js";
 7 | 
 8 | type Navigate<S = any> = (
 9 |   to: Path,
10 |   options?: { replace?: boolean; state?: S }
11 | ) => void;
12 | 
13 | type HookReturnValue = {
14 |   hook: BaseLocationHook;
15 |   searchHook: BaseSearchHook;
16 |   navigate: Navigate;
17 | };
18 | type StubHistory = { history: Path[]; reset: () => void };
19 | 
20 | export function memoryLocation(options?: {
21 |   path?: Path;
22 |   searchPath?: SearchString;
23 |   static?: boolean;
24 |   record?: false;
25 | }): HookReturnValue;
26 | export function memoryLocation(options?: {
27 |   path?: Path;
28 |   searchPath?: SearchString;
29 |   static?: boolean;
30 |   record: true;
31 | }): HookReturnValue & StubHistory;
32 | 


--------------------------------------------------------------------------------
/packages/wouter/types/router.d.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   Path,
 3 |   SearchString,
 4 |   BaseLocationHook,
 5 |   BaseSearchHook,
 6 | } from "./location-hook.js";
 7 | 
 8 | export type Parser = (
 9 |   route: Path,
10 |   loose?: boolean
11 | ) => { pattern: RegExp; keys: string[] };
12 | 
13 | export type HrefsFormatter = (href: string, router: RouterObject) => string;
14 | 
15 | // the object returned from `useRouter`
16 | export interface RouterObject {
17 |   readonly hook: BaseLocationHook;
18 |   readonly searchHook: BaseSearchHook;
19 |   readonly base: Path;
20 |   readonly ownBase: Path;
21 |   readonly parser: Parser;
22 |   readonly ssrPath?: Path;
23 |   readonly ssrSearch?: SearchString;
24 |   readonly hrefs: HrefsFormatter;
25 | }
26 | 
27 | // state captured during SSR render
28 | export type SsrContext = {
29 |   // if a redirect was encountered, this will be populated with the path
30 |   redirectTo?: Path;
31 | };
32 | 
33 | // basic options to construct a router
34 | export type RouterOptions = {
35 |   hook?: BaseLocationHook;
36 |   searchHook?: BaseSearchHook;
37 |   base?: Path;
38 |   parser?: Parser;
39 |   ssrPath?: Path;
40 |   ssrSearch?: SearchString;
41 |   ssrContext?: SsrContext;
42 |   hrefs?: HrefsFormatter;
43 | };
44 | 


--------------------------------------------------------------------------------
/packages/wouter/types/use-browser-location.d.ts:
--------------------------------------------------------------------------------
 1 | import { Path, SearchString } from "./location-hook.js";
 2 | 
 3 | type Primitive = string | number | bigint | boolean | null | undefined | symbol;
 4 | export const useLocationProperty: <S extends Primitive>(
 5 |   fn: () => S,
 6 |   ssrFn?: () => S
 7 | ) => S;
 8 | 
 9 | export type BrowserSearchHook = (options?: {
10 |   ssrSearch?: SearchString;
11 | }) => SearchString;
12 | 
13 | export const useSearch: BrowserSearchHook;
14 | 
15 | export const usePathname: (options?: { ssrPath?: Path }) => Path;
16 | 
17 | export const useHistoryState: <T = any>() => T;
18 | 
19 | export const navigate: <S = any>(
20 |   to: string | URL,
21 |   options?: { replace?: boolean; state?: S }
22 | ) => void;
23 | 
24 | /*
25 |  * Default `useLocation`
26 |  */
27 | 
28 | // The type of the default `useLocation` hook that wouter uses.
29 | // It operates on current URL using History API, supports base path and can
30 | // navigate with `pushState` or `replaceState`.
31 | export type BrowserLocationHook = (options?: {
32 |   ssrPath?: Path;
33 | }) => [Path, typeof navigate];
34 | 
35 | export const useBrowserLocation: BrowserLocationHook;
36 | 


--------------------------------------------------------------------------------
/packages/wouter/types/use-hash-location.d.ts:
--------------------------------------------------------------------------------
 1 | import { Path } from "./location-hook.js";
 2 | 
 3 | export function navigate<S = any>(
 4 |   to: Path,
 5 |   options?: { state?: S; replace?: boolean }
 6 | ): void;
 7 | 
 8 | export function useHashLocation(options?: {
 9 |   ssrPath?: Path;
10 | }): [Path, typeof navigate];
11 | 


--------------------------------------------------------------------------------
/packages/wouter/vitest.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineProject } from "vitest/config";
 2 | import react from "@vitejs/plugin-react";
 3 | 
 4 | export default defineProject({
 5 |   plugins: [react({ jsxRuntime: "automatic" })],
 6 |   test: {
 7 |     name: "wouter-react",
 8 |     setupFiles: "./setup-vitest.ts",
 9 |     environment: "happy-dom",
10 |   },
11 | });
12 | 


--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from "vitest/config";
2 | 
3 | export default defineWorkspace(["packages/*/vitest.config.ts"]);
4 | 


--------------------------------------------------------------------------------