Full Code of molefrog/wouter for AI

v3 2061eacda7f8 cached
93 files
252.1 KB
68.1k tokens
118 symbols
1 requests
Download .txt
Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
Repository: molefrog/wouter
Branch: v3
Commit: 2061eacda7f8
Files: 93
Total size: 252.1 KB

Directory structure:
gitextract_4azr5gv8/

├── .cursor/
│   └── rules/
│       ├── contributing.mdc
│       └── publishing.mdc
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci-tests.yml
│       └── size.yml
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── README.md
├── bunfig.toml
├── package.json
├── packages/
│   ├── magazin/
│   │   ├── .gitignore
│   │   ├── App.tsx
│   │   ├── client.tsx
│   │   ├── components/
│   │   │   ├── footer.tsx
│   │   │   ├── navbar.tsx
│   │   │   ├── star-wouter.tsx
│   │   │   └── with-status-code.tsx
│   │   ├── db/
│   │   │   └── products.ts
│   │   ├── index.html
│   │   ├── index.tsx
│   │   ├── package.json
│   │   ├── routes/
│   │   │   ├── 404.tsx
│   │   │   ├── about.tsx
│   │   │   ├── cart.tsx
│   │   │   ├── home.tsx
│   │   │   └── products/
│   │   │       └── [slug].tsx
│   │   ├── styles.css
│   │   └── tsconfig.json
│   ├── wouter/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.d.ts
│   │   │   ├── index.js
│   │   │   ├── memory-location.d.ts
│   │   │   ├── memory-location.js
│   │   │   ├── paths.js
│   │   │   ├── react-deps.js
│   │   │   ├── use-browser-location.d.ts
│   │   │   ├── use-browser-location.js
│   │   │   ├── use-hash-location.d.ts
│   │   │   ├── use-hash-location.js
│   │   │   ├── use-sync-external-store.js
│   │   │   └── use-sync-external-store.native.js
│   │   ├── test/
│   │   │   ├── history-patch.test.ts
│   │   │   ├── jest-dom.d.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
│   │   │   ├── setup.ts
│   │   │   ├── 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
│   │   │   └── view-transitions.test.tsx
│   │   └── 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
│   └── wouter-preact/
│       ├── .gitignore
│       ├── package.json
│       ├── src/
│       │   └── react-deps.js
│       ├── test/
│       │   └── preact.test.tsx
│       └── 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
├── specs/
│   └── view-transitions-spec.md
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .cursor/rules/contributing.mdc
================================================
---
description:
globs:
alwaysApply: true
---

# Wouter - Minimalist React Router

## Project Overview

Wouter is a **minimalist-friendly ~2.1KB React router** that focuses on performance, small bundle size, and simplicity. It provides hook-based routing for React and Preact applications.

### Key Features

- Tiny bundle size (2.1KB gzipped vs 18.7KB React Router)
- Hook-based API (`useLocation`, `useRoute`, `useParams`, etc.)
- Zero dependencies
- Supports React and Preact
- Optional top-level `<Router />` component
- Nested routing support
- SSR support
- Memory location for testing

## Project Structure

```
wouter/
├── packages/
│   ├── wouter/                 # Main React package
│   │   ├── src/
│   │   │   ├── index.js        # Main router implementation
│   │   │   ├── use-browser-location.js
│   │   │   ├── use-hash-location.js
│   │   │   ├── memory-location.js
│   │   │   ├── react-deps.js   # React imports
│   │   │   └── paths.js        # Path utilities
│   │   ├── test/               # Test files
│   │   ├── types/              # TypeScript definitions
│   │   └── esm/                # Built ES modules
│   └── wouter-preact/          # Preact package
├── assets/                     # Documentation assets
└── .github/                    # GitHub workflows
```

## Development Workflow

### Prerequisites

- Node.js v20.17.0+ or v22.9.0+
- npm

### Commands

```bash
# Install dependencies
npm install

# Run tests (watch mode)
npm test

# Run tests (single run)
npm test -- --run

# Run specific test file
npm test -- packages/wouter/test/router.test.tsx --run

# Run tests with coverage
npm test -- --coverage

# Build packages
npm run build

# Type checking
npm run typecheck

# Lint code
npm run lint
```

### Test Files Organization

- `test/router.test.tsx` - Router component and memoization tests
- `test/use-location.test.tsx` - Location hook tests
- `test/use-route.test.tsx` - Route matching tests
- `test/link.test.tsx` - Link component tests
- `test/memory-location.test.ts` - Memory location for testing
- `test/nested-route.test.tsx` - Nested routing tests
- `test/ssr.test.tsx` - Server-side rendering tests

## Key Development Principles

### Performance First

- **Bundle size matters**: Every byte counts. Use size-limit CI checks
- **Stable object references**: Prevent unnecessary re-renders with proper memoization
- **Minimize dependencies**: Zero external dependencies policy

### Code Style

- **Functional programming**: Prefer hooks over class components
- **Compact code**: Optimize for bundle size (may sacrifice readability)
- **React patterns**: Use `React.memo`, `useCallback`, `useMemo` appropriately

### Testing Guidelines

- **Comprehensive coverage**: Test all public APIs
- **Real-world scenarios**: Include complex use cases (like language switching)
- **Memory location**: Use for isolated testing
- **Memoization tests**: Verify stable object references

## Core APIs

### Hooks

- `useLocation()` - Get/set current location
- `useRoute(pattern)` - Match current location against pattern
- `useParams()` - Extract route parameters
- `useSearch()` - Get search/query string
- `useSearchParams()` - Get/set URLSearchParams
- `useRouter()` - Access router configuration

### Components

- `<Router />` - Optional configuration wrapper
- `<Route />` - Conditional rendering based on path
- `<Link />` - Navigation component
- `<Switch />` - Exclusive routing
- `<Redirect />` - Programmatic navigation

### Location Hooks

- `useBrowserLocation` - Browser history API
- `useHashLocation` - Hash-based routing
- `memoryLocation` - In-memory for testing

## Performance Considerations

### Router Memoization

The Router component uses sophisticated memoization to prevent unnecessary re-renders:

- Only creates new router objects when props actually change
- Preserves stable references for `React.memo` components
- Critical for performance in complex applications

### Bundle Optimization

- Use `// ... existing code ...` pattern in edits to minimize diffs
- Prefer shorter variable names in production builds
- Optimize for gzip/brotli compression

## Common Patterns

### Testing Router Components

```javascript
import { memoryLocation } from "wouter/memory-location";

const { hook } = memoryLocation({ path: "/test/path" });
render(
  <Router hook={hook}>
    <YourComponent />
  </Router>
);
```

### Custom Location Hooks

```javascript
const useCustomLocation = () => {
  const [location, setLocation] = useBrowserLocation();
  // Add custom logic here
  return [location, setLocation];
};
```

### Nested Routing

```javascript
<Route path="/app" nest>
  <Route path="/users/:id">
    <UserProfile />
  </Route>
</Route>
```

## Debugging Tips

### Common Issues

1. **Router memoization**: Check if unnecessary re-renders occur
2. **Base path conflicts**: Verify base path inheritance in nested routers
3. **Pattern matching**: Use regexparam syntax for route patterns
4. **SSR hydration**: Ensure client/server router configs match

### Debugging Tools

- React DevTools Profiler for render tracking
- Network tab for bundle size verification
- Console warnings for deprecated patterns

## Contributing Guidelines

### Before Making Changes

1. Run full test suite: `npm test -- --run`
2. Check bundle size impact
3. Verify TypeScript definitions
4. Test with both React and Preact

### Adding New Features

1. Consider bundle size impact
2. Add comprehensive tests
3. Update TypeScript definitions
4. Document in README if public API
5. Maintain backward compatibility

### Performance Changes

1. Add before/after performance tests
2. Verify memoization behavior
3. Test with complex nested scenarios
4. Check for memory leaks in long-running apps

## Architecture Notes

### Router Context System

- Default router created on-demand
- Context inheritance with override capabilities
- Memoization prevents cascade re-renders
- Base path accumulation in nested routers

### Pattern Matching

- Uses regexparam library internally
- Supports named parameters, wildcards, optional segments
- Regex patterns supported for complex matching
- Loose mode for nested routing

This project prioritizes performance and minimalism while maintaining full feature parity with larger routing solutions.


================================================
FILE: .cursor/rules/publishing.mdc
================================================
---
description: 
globs: 
alwaysApply: true
---
# Publishing Wouter Packages

This document outlines the process for publishing new versions of the wouter packages to npm.

## Prerequisites

- Ensure you have npm publish permissions for both `wouter` and `wouter-preact` packages
- Have npm authentication set up (you'll need OTP access)
- All tests should be passing
- All changes should be committed to the repository

## Publishing Process

### 1. Version Bump

Update the version in both package.json files:
- `packages/wouter/package.json`
- `packages/wouter-preact/package.json`

For semantic versioning:
- **Patch** (x.x.X): Bug fixes, small improvements
- **Minor** (x.X.x): New features, backward compatible
- **Major** (X.x.x): Breaking changes

### 2. Dry Run Validation

Run dry publish commands to validate the packages before actual publishing:

```bash
npm publish --dry-run -w wouter
npm publish --dry-run -w wouter-preact
```

This will show you:
- Package contents and file list
- Bundle size information
- Version confirmation
- Any potential issues

### 3. Build Verification (Optional)

Check the build artifacts in the `/esm/` folders to ensure the latest updates are properly built:
- `packages/wouter/esm/`
- `packages/wouter-preact/esm/`

The `prepublishOnly` script automatically builds the packages, but it's good to verify.

## ⚠️ Confirmation Required

**ALWAYS ask for user confirmation before proceeding with the following steps:**

### 4. Publish to npm

Publish both packages to the npm registry:

```bash
npm publish -w wouter
npm publish -w wouter-preact
```

**Note:** npm might fail with "EOTP" (Error One-Time Password) even when the OTP was entered correctly in the browser. This is a known npm issue - the packages are usually still published successfully despite the error message.

### 5. Git Commit and Tag

After successful publishing:

1. **Commit the version changes:**
   ```bash
   git add packages/wouter/package.json packages/wouter-preact/package.json
   git commit -m "Bump version to X.X.X"
   ```

2. **Create a version tag:**
   ```bash
   git tag vX.X.X
   ```

3. **Push to remote (optional):**
   ```bash
   git push origin main
   git push origin vX.X.X
   ```

## Troubleshooting

### Common Issues

- **Authentication errors**: Run `npm login` or use the browser authentication flow
- **OTP timeout**: Generate a fresh OTP code from your authenticator app
- **Permission denied**: Ensure you have publish permissions for both packages
- **Version conflicts**: Check if the version already exists on npm

### Verification

After publishing, verify the packages are available:
- Check [wouter on npm](mdc:https:/www.npmjs.com/package/wouter)
- Check [wouter-preact on npm](mdc:https:/www.npmjs.com/package/wouter-preact)
- Test installation: `npm install wouter@latest`

## Package Information

- **wouter**: Main React package (~22KB package size)
- **wouter-preact**: Preact-specific package (~22KB package size)
- Both packages maintain version parity and should be published together

================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: ["molefrog"]


================================================
FILE: .github/workflows/ci-tests.yml
================================================
name: Run Tests & Linters

on:
  push:
    branches: "*"
  pull_request:
    branches: "*"

jobs:
  build:
    runs-on: ubuntu-latest

    env:
      FORCE_COLOR: true

    steps:
      - uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Install Dependencies
        run: bun install --frozen-lockfile

      - name: Prepare wouter-preact (copy source files)
        run: cd packages/wouter-preact && npm run prepublishOnly

      - name: Run test
        run: bun test --coverage --coverage-reporter=lcov --coverage-reporter=text

      - name: Run type check
        run: bun run test-types

      - name: Lint Sources with ESLint
        run: bun run lint

      - name: Upload Coverage to Coveralls
        uses: coverallsapp/github-action@v2
        with:
          file: ./coverage/lcov.info


================================================
FILE: .github/workflows/size.yml
================================================
name: Size
on: [pull_request]
jobs:
  size:
    runs-on: ubuntu-latest
    env:
      CI_JOB_NUMBER: 1
    steps:
      - uses: actions/checkout@v3
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - name: Install Dependencies
        run: bun install --frozen-lockfile
      - name: Prepare wouter-preact (copy source files)
        run: cd packages/wouter-preact && npm run prepublishOnly
      - name: Symlink npm to bun
        run: |
          sudo ln -sf $(which bun) /usr/local/bin/npm
          sudo ln -sf $(which bunx) /usr/local/bin/npx
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          skip_step: install
          build_script: build


================================================
FILE: .gitignore
================================================
# NPM
npm-debug.log*
node_modules/
.npm

# IDEs
.vscode/
*.code-workspace

# bundler
esm/
.cache

# OSX
.DS_Store
.AppleDouble
.LSOverride

# test coverage
coverage/

# type definitions in the project root are copied from types/ folder
# before publishing, so they should be ignored
/*.d.ts

# README is copied from the root folder
packages/wouter/README.md
packages/wouter-preact/README.md


================================================
FILE: CLAUDE.md
================================================
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---

Default to using Bun instead of Node.js.

- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.

## APIs

- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.

## Testing

Use `bun test` to run tests.

```ts#index.test.ts
import { test, expect } from "bun:test";

test("hello world", () => {
  expect(1).toBe(1);
});
```

## Frontend

Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.

Server:

```ts#index.ts
import index from "./index.html"

Bun.serve({
  routes: {
    "/": index,
    "/api/users/:id": {
      GET: (req) => {
        return new Response(JSON.stringify({ id: req.params.id }));
      },
    },
  },
  // optional websocket support
  websocket: {
    open: (ws) => {
      ws.send("Hello, world!");
    },
    message: (ws, message) => {
      ws.send(message);
    },
    close: (ws) => {
      // handle close
    }
  },
  development: {
    hmr: true,
    console: true,
  }
})
```

HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.

```html#index.html
<html>
  <body>
    <h1>Hello, world!</h1>
    <script type="module" src="./frontend.tsx"></script>
  </body>
</html>
```

With the following `frontend.tsx`:

```tsx#frontend.tsx
import React from "react";

// import .css files directly and it works
import './index.css';

import { createRoot } from "react-dom/client";

const root = createRoot(document.body);

export default function Frontend() {
  return <h1>Hello, world!</h1>;
}

root.render(<Frontend />);
```

Then, run index.ts

```sh
bun --hot ./index.ts
```

For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.


================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <https://unlicense.org>


================================================
FILE: README.md
================================================
<div align="center">
  <img src="assets/logo.svg" width="80" alt="Wouter — a super-tiny React router (logo by Katya Simacheva)" />
</div>

<br />

<div align="center">
  <a href="https://npmjs.org/package/wouter"><img alt="npm" src="https://img.shields.io/npm/v/wouter.svg?color=black&labelColor=888" /></a>
  <a href="https://travis-ci.org/molefrog/wouter"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/molefrog/wouter/size.yml?color=black&labelColor=888&label=2.5KB+limit" /></a>
  <a href="https://coveralls.io/github/molefrog/wouter?branch=v3"><img alt="Coverage" src="https://img.shields.io/coveralls/github/molefrog/wouter/v3.svg?color=black&labelColor=888" /></a>
  <a href="https://www.npmjs.com/package/wouter"><img alt="Coverage" src="https://img.shields.io/npm/dm/wouter.svg?color=black&labelColor=888" /></a>
  <a href="https://pr.new/molefrog/wouter"><img alt="Edit in StackBlitz IDE" src="https://img.shields.io/badge/StackBlitz-New%20PR-black?labelColor=888" /></a>
</div>

<div align="center">
  <b>wouter</b> is a tiny router for modern React and Preact apps that relies on Hooks. <br />
  A router you wanted so bad in your project!<br>
</div>

## Features

<img src="assets/wouter.svg" align="right" width="250" alt="by Katya Simacheva" />

- Minimum dependencies, only **2.1 KB** gzipped vs 18.7KB
  [React Router](https://github.com/ReactTraining/react-router).
- Supports both **React** and **[Preact](https://preactjs.com/)**! Read
  _["Preact support" section](#preact-support)_ for more details.
- No top-level `<Router />` component, it is **fully optional**.
- Mimics [React Router](https://github.com/ReactTraining/react-router)'s best practices by providing
  familiar **[`Route`](#route-pathpattern-)**, **[`Link`](#link-hrefpath-)**,
  **[`Switch`](#switch-)** and **[`Redirect`](#redirect-topath-)** components.
- Has hook-based API for more granular control over routing (like animations):
  **[`useLocation`](#uselocation-working-with-the-history)**,
  **[`useRoute`](#useroute-route-matching-and-parameters)** and
  **[`useRouter`](#userouter-accessing-the-router-object)**.

## developers :sparkling_heart: wouter

> ... I love Wouter. It’s tiny, fully embraces hooks, and has an intuitive and barebones API. I can
> accomplish everything I could with react-router with Wouter, and it just feels **more minimalist
> while not being inconvenient.**
>
> [**Matt Miller**, _An exhaustive React ecosystem for 2020_](https://medium.com/@mmiller42/an-exhaustive-react-guide-for-2020-7859f0bddc56)

Wouter provides a simple API that many developers and library authors appreciate. Some notable
projects that use wouter: **[Ultra](https://ultrajs.dev/)**,
**[React-three-fiber](https://github.com/react-spring/react-three-fiber)**,
**[Sunmao UI](https://sunmao-ui.com/)**, **[Million](https://million.dev/)** and many more.

## Table of Contents

- [Getting Started](#getting-started)
  - [Browser Support](#browser-support)
- [Wouter API](#wouter-api)
  - [The list of methods available](#the-list-of-methods-available)
- [Hooks API](#hooks-api)
  - [`useRoute`: route matching and parameters](#useroute-route-matching-and-parameters)
  - [`useLocation`: working with the history](#uselocation-working-with-the-history)
    - [Additional navigation parameters](#additional-navigation-parameters)
    - [Customizing the location hook](#customizing-the-location-hook)
  - [`useParams`: extracting matched parameters](#useparams-extracting-matched-parameters)
  - [`useSearch`: query strings](#usesearch-query-strings)
  - [`useSearchParams`: search parameters](#usesearchparams-search-parameters)
  - [`useRouter`: accessing the router object](#userouter-accessing-the-router-object)
- [Component API](#component-api)

  - [`<Route path={pattern} />`](#route-pathpattern-)
    - [Route nesting](#route-nesting)
  - [`<Link href={path} />`](#link-hrefpath-)
  - [`<Switch />`](#switch-)
  - [`<Redirect to={path} />`](#redirect-topath-)
  - [`<Router hook={hook} parser={fn} base={basepath} />`](#router-hookhook-parserfn-basebasepath-hrefsfn-)

- [FAQ and Code Recipes](#faq-and-code-recipes)
  - [I deploy my app to the subfolder. Can I specify a base path?](#i-deploy-my-app-to-the-subfolder-can-i-specify-a-base-path)
  - [How do I make a default route?](#how-do-i-make-a-default-route)
  - [How do I make a link active for the current route?](#how-do-i-make-a-link-active-for-the-current-route)
  - [Are strict routes supported?](#are-strict-routes-supported)
  - [Are relative routes and links supported?](#are-relative-routes-and-links-supported)
  - [Can I initiate navigation from outside a component?](#can-i-initiate-navigation-from-outside-a-component)
  - [Can I use _wouter_ in my TypeScript project?](#can-i-use-wouter-in-my-typescript-project)
  - [How can add animated route transitions?](#how-can-add-animated-route-transitions)
  - [How do I add view transitions to my app?](#how-do-i-add-view-transitions-to-my-app)
  - [Preact support?](#preact-support)
  - [Server-side Rendering support (SSR)?](#server-side-rendering-support-ssr)
  - [How do I configure the router to render a specific route in tests?](#how-do-i-configure-the-router-to-render-a-specific-route-in-tests)
  - [1KB is too much, I can't afford it!](#1kb-is-too-much-i-cant-afford-it)
- [Acknowledgements](#acknowledgements)

## Getting Started

First, add wouter to your project.

```bash
npm i wouter
```

Or, if you're using Preact the use the following command [`npm i wouter-preact`](#preact-support).

Check out this simple demo app below. It doesn't cover hooks and other features such as nested routing, but it's a good starting point for those who are migrating from React Router.

```js
import { Link, Route, Switch } from "wouter";

const App = () => (
  <>
    <Link href="/users/1">Profile</Link>

    <Route path="/about">About Us</Route>

    {/* 
      Routes below are matched exclusively -
      the first matched route gets rendered
    */}
    <Switch>
      <Route path="/inbox" component={InboxPage} />

      <Route path="/users/:name">
        {(params) => <>Hello, {params.name}!</>}
      </Route>

      {/* Default route in a switch */}
      <Route>404: No such page!</Route>
    </Switch>
  </>
);
```

### Browser Support

This library is designed for **ES2020+** compatibility. If you need to support older browsers, make sure that you transpile `node_modules`. Additionally, the minimum supported TypeScript version is 4.1 in order to support route parameter inference.

## Wouter API

Wouter comes with three kinds of APIs: low-level **standalone location hooks**, hooks for **routing and pattern matching** and more traditional **component-based
API** similar to React Router's one.

You are free to choose whatever works for you: use location hooks when you want to keep your app as small as
possible and don't need pattern matching; use routing hooks when you want to build custom routing components; or if you're building a traditional app
with pages and navigation — components might come in handy.

Check out also [FAQ and Code Recipes](#faq-and-code-recipes) for more advanced things like active
links, default routes, server-side rendering etc.

### The list of methods available

**Location Hooks**

These can be used separately from the main module and have an interface similar to `useState`. These hooks are standalone and don't include built-in support for nesting, base path, or route matching. However, when passed to `<Router>`, they work seamlessly with all Router features including nesting and base paths.

- **[`import { useBrowserLocation } from "wouter/use-browser-location"`](https://github.com/molefrog/wouter/blob/v3/packages/wouter/src/use-browser-location.js)** —
  allows to manipulate current location in the browser's address bar, a tiny wrapper around the History API.
- **[`import { useHashLocation } from "wouter/use-hash-location"`](https://github.com/molefrog/wouter/blob/v3/packages/wouter/src/use-hash-location.js)** — similarly, gets location from the hash part of the address, i.e. the string after a `#`.
- **[`import { memoryLocation } from "wouter/memory-location"`](#uselocation-working-with-the-history)** — an in-memory location hook with history support, external navigation and immutable mode for testing. **Note** the module name because it is a high-order hook. See how memory location can be used in [testing](#how-do-i-configure-the-router-to-render-a-specific-route-in-tests).

**Routing Hooks**

Import from `wouter` module.

- **[`useRoute`](#useroute-the-power-of-hooks)** — shows whether or not current page matches the
  pattern provided.
- **[`useLocation`](#uselocation-working-with-the-history)** — allows to manipulate current
  router's location, by default subscribes to browser location. **Note:** this isn't the same as `useBrowserLocation`, read below.
- **[`useParams`](#useparams-extracting-matched-parameters)** — returns an object with parameters matched from the closest route.
- **[`useSearch`](#usesearch-query-strings)** — returns a search string – everything that goes after the `?`.
- **[`useRouter`](#userouter-accessing-the-router-object)** — returns a global router object that
  holds the configuration. Only use it if you want to customize the routing.

**Components**

Import from `wouter` module.

- **[`<Route />`](#route-pathpattern-)** — conditionally renders a component based on a pattern.
- **[`<Link />`](#link-hrefpath-)** — wraps `<a>`, allows to perform a navigation.
- **[`<Switch />`](#switch-)** — exclusive routing, only renders the first matched route.
- **[`<Redirect />`](#redirect-topath-)** — when rendered, performs an immediate navigation.
- **[`<Router />`](#router-hookhook-matchermatchfn-basebasepath-)** — an optional top-level
  component for advanced routing configuration.

## Hooks API

### `useRoute`: route matching and parameters

Checks if the current location matches the pattern provided and returns an object with parameters. This is powered by a wonderful [`regexparam`](https://github.com/lukeed/regexparam) library, so all its pattern syntax is fully supported.

You can use `useRoute` to perform manual routing or implement custom logic, such as route transitions, etc.

```js
import { useRoute } from "wouter";

const Users = () => {
  // `match` is a boolean
  const [match, params] = useRoute("/users/:name");

  if (match) {
    return <>Hello, {params.name}!</>;
  } else {
    return null;
  }
};
```

A quick cheatsheet of what types of segments are supported:

```js
useRoute("/app/:page");
useRoute("/app/:page/:section");

// optional parameter, matches "/en/home" and "/home"
useRoute("/:locale?/home");

// suffixes
useRoute("/movies/:title.(mp4|mov)");

// wildcards, matches "/app", "/app-1", "/app/home"
useRoute("/app*");

// optional wildcards, matches "/orders", "/orders/"
// and "/orders/completed/list"
useRoute("/orders/*?");

// regex for matching complex patterns,
// matches "/hello:123"
useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/);
// and with named capture groups
useRoute(/^[/](?<word>[a-z]+):(?<num>[0-9]+)[/]?$/);
```

The second item in the pair `params` is an object with parameters or null if there was no match. For wildcard segments the parameter name is `"*"`:

```js
// wildcards, matches "/app", "/app-1", "/app/home"
const [match, params] = useRoute("/app*");

if (match) {
  // "/home" for "/app/home"
  const page = params["*"];
}
```

### `useLocation`: working with the history

To get the current path and navigate between pages, call the `useLocation` hook. Similarly to `useState`, it returns a value and a setter: the component will re-render when the location changes and by calling `navigate` you can update this value and perform navigation.

By default, it uses `useBrowserLocation` under the hood, though you can configure this in a top-level `Router` component (for example, if you decide at some point to switch to a hash-based routing). `useLocation` will also return scoped path when used within nested routes or with base path setting.

```js
import { useLocation } from "wouter";

const CurrentLocation = () => {
  const [location, navigate] = useLocation();

  return (
    <div>
      {`The current page is: ${location}`}
      <a onClick={() => navigate("/somewhere")}>Click to update</a>
    </div>
  );
};
```

All the components internally call the `useLocation` hook.

#### Additional navigation parameters

The setter method of `useLocation` can also accept an optional object with parameters to control how
the navigation update will happen.

When browser location is used (default), `useLocation` hook accepts `replace` flag to tell the hook to modify the current
history entry instead of adding a new one. It is the same as calling `replaceState`.

```jsx
const [location, navigate] = useLocation();

navigate("/jobs"); // `pushState` is used
navigate("/home", { replace: true }); // `replaceState` is used
```

Additionally, you can provide a `state` option to update `history.state` while navigating:

```jsx
navigate("/home", { state: { modal: "promo" } });

history.state; // { modal: "promo" }
```

#### Customizing the location hook

By default, **wouter** uses `useLocation` hook that reacts to `pushState` and `replaceState`
navigation via `useBrowserLocation`.

To customize this, wrap your app in a `Router` component:

```js
import { Router, Route } from "wouter";
import { useHashLocation } from "wouter/use-hash-location";

const App = () => (
  <Router hook={useHashLocation}>
    <Route path="/about" component={About} />
    ...
  </Router>
);
```

Because these hooks have return values similar to `useState`, it is easy and fun to build your own location hooks: `useCrossTabLocation`, `useLocalStorage`, `useMicroFrontendLocation` and whatever routing logic you want to support in the app. Give it a try!

### `useParams`: extracting matched parameters

This hook allows you to access the parameters exposed through [matching dynamic segments](#matching-dynamic-segments). Internally, we simply wrap your components in a context provider allowing you to access this data anywhere within the `Route` component.

This allows you to avoid "prop drilling" when dealing with deeply nested components within the route. **Note:** `useParams` will only extract parameters from the closest parent route.

```js
import { Route, useParams } from "wouter";

const User = () => {
  const params = useParams();

  params.id; // "1"

  // alternatively, use the index to access the prop
  params[0]; // "1"
};

<Route path="/user/:id" component={User}> />
```

It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead.

```js
import { Route, useParams } from "wouter";

const User = () => {
  const params = useParams();

  params.id; // "1"
  params[0]; // "1"
};

<Route path={/^[/]user[/](?<id>[0-9]+)[/]?$/} component={User}> />
```

### `useSearch`: query strings

Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returned **does not** contain a `?` character.

```jsx
import { useSearch } from "wouter";

// returns "tab=settings&id=1"
const searchString = useSearch();
```

For the SSR, use `ssrSearch` prop passed to the router.

```jsx
<Router ssrSearch={request.search}>{/* SSR! */}</Router>
```

Refer to [Server-Side Rendering](#server-side-rendering-support-ssr) for more info on rendering and hydration.

### `useSearchParams`: search parameters

Returns a `URLSearchParams` object and a setter function to update search parameters. The setter accepts either a value (object, URLSearchParams, string[][], etc.) or a **callback function** that receives the current params and must return the new params.

```jsx
import { useSearchParams } from 'wouter';

const [searchParams, setSearchParams] = useSearchParams();

// extract a specific search parameter
const id = searchParams.get('id');

// modify a specific search parameter
setSearchParams((prev) => {
  prev.set('tab', 'settings');
  return prev;
});

// override all search parameters
setSearchParams({
  id: 1234,
  tab: 'settings',
});

// by default, setSearchParams() will push a new history entry
// to avoid this, set `replace` option to `true`
setSearchParams(
  (prev) => {
    prev.set('order', 'desc');
    return prev;
  },
  {
    replace: true,
  },
);

// you can also pass a history state in options
setSearchParams(
  (prev) => {
    prev.set('foo', 'bar');
    return prev;
  },
  {
    state: 'hello',
  },
);
```

### `useRouter`: accessing the router object

If you're building advanced integration, for example custom location hook, you might want to get
access to the global router object. Router is a simple object that holds routing options that you configure in the `Router` component.

```js
import { useRouter } from "wouter";

const Custom = () => {
  const router = useRouter();

  router.hook; // `useBrowserLocation` by default
  router.base; // "/app"
};

const App = () => (
  <Router base="/app">
    <Custom />
  </Router>
);
```

## Component API

### `<Route path={pattern} />`

`Route` represents a piece of the app that is rendered conditionally based on a pattern `path`. Pattern has the same syntax as the argument you pass to [`useRoute`](#useroute-route-matching-and-parameters).

The library provides multiple ways to declare a route's body:

```js
import { Route } from "wouter";

// simple form
<Route path="/home"><Home /></Route>

// render-prop style
<Route path="/users/:id">
  {params => <UserPage id={params.id} />}
</Route>

// the `params` prop will be passed down to <Orders />
<Route path="/orders/:status" component={Orders} />
```

A route with no path is considered to always match, and it is the same as `<Route path="*" />`. When developing your app, use this trick to peek at the route's content without navigation.

```diff
-<Route path="/some/page">
+<Route>
  {/* Strip out the `path` to make this visible */}
</Route>
```

#### Route Nesting

Nesting is a core feature of wouter and can be enabled on a route via the `nest` prop. When this prop is present, the route matches everything that starts with a given pattern and it creates a nested routing context. All child routes will receive location relative to that pattern.

Let's take a look at this example:

```js
<Route path="/app" nest>
  <Route path="/users/:id" nest>
    <Route path="/orders" />
  </Route>
</Route>
```

1. This first route will be active for all paths that start with `/app`, this is equivalent to having a base path in your app.

2. The second one uses dynamic pattern to match paths like `/app/user/1`, `/app/user/1/anything` and so on.

3. Finally, the inner-most route will only work for paths that look like `/app/users/1/orders`. The match is strict, since that route does not have a `nest` prop and it works as usual.

If you call `useLocation()` inside the last route, it will return `/orders` and not `/app/users/1/orders`. This creates a nice isolation and it makes it easier to make changes to parent route without worrying that the rest of the app will stop working. If you need to navigate to a top-level page however, you can use a prefix `~` to refer to an absolute path:

```js
<Route path="/payments" nest>
  <Route path="/all">
    <Link to="~/home">Back to Home</Link>
  </Route>
</Route>
```

**Note:** The `nest` prop does not alter the regex passed into regex paths.
Instead, the `nest` prop will only determine if nested routes will match against the rest of path or the same path.
To make a strict path regex, use a regex pattern like `/^[/](your pattern)[/]?$/` (this matches an optional end slash and the end of the string).
To make a nestable regex, use a regex pattern like `/^[/](your pattern)(?=$|[/])/` (this matches either the end of the string or a slash for future segments).

### `<Link href={path} />`

Link component renders an `<a />` element that, when clicked, performs a navigation.

```js
import { Link } from "wouter"

<Link href="/">Home</Link>

// `to` is an alias for `href`
<Link to="/">Home</Link>

// all standard `a` props are proxied
<Link href="/" className="link" aria-label="Go to homepage">Home</Link>

// all location hook options are supported
<Link href="/" replace state={{ animate: true }} />
```

Link will always wrap its children in an `<a />` tag, unless `asChild` prop is provided. Use this when you need to have a custom component that renders an `<a />` under the hood.

```jsx
// use this instead
<Link to="/" asChild>
  <UIKitLink />
</Link>

// Remember, `UIKitLink` must implement an `onClick` handler
// in order for navigation to work!
```

When you pass a function as a `className` prop, it will be called with a boolean value indicating whether the link is active for the current route. You can use this to style active links (e.g. for links in navigation menu)

```jsx
<Link className={(active) => (active ? "active" : "")}>Nav</Link>
```

Read more about [active links here](#how-do-i-make-a-link-active-for-the-current-route).

### `<Switch />`

There are cases when you want to have an exclusive routing: to make sure that only one route is
rendered at the time, even if the routes have patterns that overlap. That's what `Switch` does: it
only renders **the first matching route**.

```js
import { Route, Switch } from "wouter";

<Switch>
  <Route path="/orders/all" component={AllOrders} />
  <Route path="/orders/:status" component={Orders} />

  {/* 
     in wouter, any Route with empty path is considered always active. 
     This can be used to achieve "default" route behaviour within Switch. 
     Note: the order matters! See examples below.
  */}
  <Route>This is rendered when nothing above has matched</Route>
</Switch>;
```

When no route in switch matches, the last empty `Route` will be used as a fallback. See [**FAQ and Code Recipes** section](#how-do-i-make-a-default-route) to read about default routes.

### `<Redirect to={path} />`

When mounted performs a redirect to a `path` provided. Uses `useLocation` hook internally to trigger
the navigation inside of a `useEffect` block.

`Redirect` can also accept props for [customizing how navigation will be performed](#additional-navigation-parameters), for example for setting history state when navigating. These options are specific to the currently used location hook.

```jsx
<Redirect to="/" />

// arbitrary state object
<Redirect to="/" state={{ modal: true }} />

// use `replaceState`
<Redirect to="/" replace />
```

If you need more advanced logic for navigation, for example, to trigger the redirect inside of an
event handler, consider using
[`useLocation` hook instead](#uselocation-working-with-the-history):

```js
import { useLocation } from "wouter";

const [location, setLocation] = useLocation();

fetchOrders().then((orders) => {
  setOrders(orders);
  setLocation("/app/orders");
});
```

### `<Router hook={hook} parser={fn} base={basepath} hrefs={fn} />`

Unlike _React Router_, routes in wouter **don't have to be wrapped in a top-level component**. An
internal router object will be constructed on demand, so you can start writing your app without
polluting it with a cascade of top-level providers. There are cases however, when the routing
behaviour needs to be customized.

These cases include hash-based routing, basepath support, custom matcher function etc.

```jsx
import { useHashLocation } from "wouter/use-hash-location";

<Router hook={useHashLocation} base="/app">
  {/* Your app goes here */}
</Router>;
```

A router is a simple object that holds the routing configuration options. You can always obtain this
object using a [`useRouter` hook](#userouter-accessing-the-router-object). The list of currently
available options:

- **`hook: () => [location: string, setLocation: fn]`** — is a React Hook function that subscribes
  to location changes. It returns a pair of current `location` string e.g. `/app/users` and a
  `setLocation` function for navigation. You can use this hook from any component of your app by
  calling [`useLocation()` hook](#uselocation-working-with-the-history). See [Customizing the location hook](#customizing-the-location-hook).

- **`searchHook: () => [search: string, setSearch: fn]`** — similar to `hook`, but for obtaining the [current search string](#usesearch-query-strings).

- **`base: string`** — an optional setting that allows to specify a base path, such as `/app`. All
  application routes will be relative to that path. To navigate out to an absolute path, prefix your path with an `~`. [See the FAQ](#are-relative-routes-and-links-supported).

- **`parser: (path: string, loose?: boolean) => { pattern, keys }`** — a pattern parsing
  function. Produces a RegExp for matching the current location against the user-defined patterns like
  `/app/users/:id`. Has the same interface as the [`parse`](https://github.com/lukeed/regexparam?tab=readme-ov-file#regexparamparseinput-regexp) function from `regexparam`. See [this example](#are-strict-routes-supported) that demonstrates custom parser feature.

- **`ssrPath: string`** and **`ssrSearch: string`** use these when [rendering your app on the server](#server-side-rendering-support-ssr).

- `hrefs: (href: boolean) => string` — a function for transforming `href` attribute of an `<a />` element rendered by `Link`. It is used to support hash-based routing. By default, `href` attribute is the same as the `href` or `to` prop of a `Link`. A location hook can also define a `hook.hrefs` property, in this case the `href` will be inferred.

- **`aroundNav: (navigate, to, options) => void`** — a handler that wraps all navigation calls. Use this to intercept navigation and perform custom logic before and after the navigation occurs. You can modify navigation parameters, add side effects, or prevent navigation entirely. This is particularly useful for implementing [view transitions](#how-do-i-add-view-transitions-to-my-app). By default, it simply calls `navigate(to, options)`.

  ```js
  const aroundNav = (navigate, to, options) => {
    // do something before navigation
    navigate(to, options); // perform navigation
    // do something after navigation
  };
  ```

## FAQ and Code Recipes

### I deploy my app to the subfolder. Can I specify a base path?

You can! Wrap your app with `<Router base="/app" />` component and that should do the trick:

```js
import { Router, Route, Link } from "wouter";

const App = () => (
  <Router base="/app">
    {/* the link's href attribute will be "/app/users" */}
    <Link href="/users">Users</Link>

    <Route path="/users">The current path is /app/users!</Route>
  </Router>
);
```

Calling `useLocation()` within a route in an app with base path will return a path scoped to the base. Meaning that when base is `"/app"` and pathname is `"/app/users"` the returned string is `"/users"`. Accordingly, calling `navigate` will automatically append the base to the path argument for you.

When you have multiple nested routers, base paths are inherited and stack up.

```js
<Router base="/app">
  <Router base="/cms">
    <Route path="/users">Path is /app/cms/users!</Route>
  </Router>
</Router>
```

### How do I make a default route?

One of the common patterns in application routing is having a default route that will be shown as a
fallback, in case no other route matches (for example, if you need to render 404 message). In
**wouter** this can easily be done as a combination of `<Switch />` component and a default route:

```js
import { Switch, Route } from "wouter";

<Switch>
  <Route path="/about">...</Route>
  <Route>404, Not Found!</Route>
</Switch>;
```

_Note:_ the order of switch children matters, default route should always come last.

If you want to have access to the matched segment of the path you can use wildcard parameters:

```js
<Switch>
  <Route path="/users">...</Route>

  {/* will match anything that starts with /users/, e.g. /users/foo, /users/1/edit etc. */}
  <Route path="/users/*">...</Route>

  {/* will match everything else */}
  <Route path="*">
    {(params) => `404, Sorry the page ${params["*"]} does not exist!`}
  </Route>
</Switch>
```

**[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-ts-8q532r)**

### How do I make a link active for the current route?

Instead of a regular `className` string, provide a function to use custom class when this link matches the current route. Note that it will always perform an exact match (i.e. `/users` will not be active for `/users/1`).

```jsx
<Link className={(active) => (active ? "active" : "")}>Nav link</Link>
```

If you need to control other props, such as `aria-current` or `style`, you can write your own `<Link />` wrapper
and detect if the path is active by using the `useRoute` hook.

```js
const [isActive] = useRoute(props.href);

return (
  <Link {...props} asChild>
    <a style={isActive ? { color: "red" } : {}}>{props.children}</a>
  </Link>
);
```

**[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-ts-8q532r?file=/src/ActiveLink.tsx)**

### Are strict routes supported?

If a trailing slash is important for your app's routing, you could specify a custom parser. Parser is a method that takes a pattern string and returns a RegExp and an array of parsed key. It uses the signature of a [`parse`](https://github.com/lukeed/regexparam?tab=readme-ov-file#regexparamparseinput-regexp) function from `regexparam`.

Let's write a custom parser based on a popular [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) package that does support strict routes option.

```js
import { pathToRegexp } from "path-to-regexp";

/**
 * Custom parser based on `pathToRegexp` with strict route option
 */
const strictParser = (path, loose) => {
  const keys = [];
  const pattern = pathToRegexp(path, keys, { strict: true, end: !loose });

  return {
    pattern,
    // `pathToRegexp` returns some metadata about the keys,
    // we want to strip it to just an array of keys
    keys: keys.map((k) => k.name),
  };
};

const App = () => (
  <Router parser={strictParser}>
    <Route path="/foo">...</Route>
    <Route path="/foo/">...</Route>
  </Router>
);
```

**[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-strict-routes-w3xdtz)**

### Are relative routes and links supported?

Yes! Any route with `nest` prop present creates a nesting context. Keep in mind, that the location inside a nested route will be scoped.

```js
const App = () => (
  <Router base="/app">
    <Route path="/dashboard" nest>
      {/* the href is "/app/dashboard/users" */}
      <Link to="/users" />

      <Route path="/users">
        {/* Here `useLocation()` returns "/users"! */}
      </Route>
    </Route>
  </Router>
);
```

**[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-nested-routes-l8p23s)**

### Can I initiate navigation from outside a component?

Yes, the `navigate` function is exposed from the `"wouter/use-browser-location"` module:

```js
import { navigate } from "wouter/use-browser-location";

navigate("/", { replace: true });
```

It's the same function that is used internally.

### Can I use _wouter_ in my TypeScript project?

Yes! Although the project isn't written in TypeScript, the type definition files are bundled with
the package.

### How can add animated route transitions?

Let's take look at how wouter routes can be animated with [`framer-motion`](framer.com/motion).
Animating enter transitions is easy, but exit transitions require a bit more work. We'll use the `AnimatePresence` component that will keep the page in the DOM until the exit animation is complete.

Unfortunately, `AnimatePresence` only animates its **direct children**, so this won't work:

```jsx
import { motion, AnimatePresence } from "framer-motion";

export const MyComponent = () => (
  <AnimatePresence>
    {/* This will not work! `motion.div` is not a direct child */}
    <Route path="/">
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      />
    </Route>
  </AnimatePresence>
);
```

The workaround is to match this route manually with `useRoute`:

```jsx
export const MyComponent = ({ isVisible }) => {
  const [isMatch] = useRoute("/");

  return (
    <AnimatePresence>
      {isMatch && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      )}
    </AnimatePresence>
  );
};
```

More complex examples involve using `useRoutes` hook (similar to how React Router does it), but wouter does not ship it out-of-the-box. Please refer to [this issue](https://github.com/molefrog/wouter/issues/414#issuecomment-1954192679) for the workaround.

### How do I use wouter with View Transitions API?

Wouter works seamlessly with the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API), but you'll need to manually activate it. This is because view transitions require synchronous DOM rendering and must be wrapped in `flushSync` from `react-dom`. Following wouter's philosophy of staying lightweight and avoiding unnecessary dependencies, view transitions aren't built-in. However, there's a simple escape hatch to enable them: the `aroundNav` prop.

```jsx
import { flushSync } from "react-dom";
import { Router, type AroundNavHandler } from "wouter";

const aroundNav: AroundNavHandler = (navigate, to, options) => {
  // Check if View Transitions API is supported
  if (!document.startViewTransition) {
    navigate(to, options);
    return;
  }

  document.startViewTransition(() => {
    flushSync(() => {
      navigate(to, options);
    });
  });
};

const App = () => (
  <Router aroundNav={aroundNav}>
    {/* Your routes here */}
  </Router>
);
```

You can also enable transitions selectively using the `transition` prop, which will be available in the `options` parameter:

```jsx
// Enable transition for a specific link
<Link to="/about" transition>About</Link>

// Or programmatically
const [location, navigate] = useLocation();
navigate("/about", { transition: true });

// Then check for it in your handler
const aroundNav: AroundNavHandler = (navigate, to, options) => {
  if (!document.startViewTransition) {
    navigate(to, options);
    return;
  }

  if (options?.transition) {
    document.startViewTransition(() => {
      flushSync(() => {
        navigate(to, options);
      });
    });
  } else {
    navigate(to, options);
  }
};
```

### Preact support?

Preact exports are available through a separate package named `wouter-preact` (or within the
`wouter/preact` namespace, however this method isn't recommended as it requires React as a peer
dependency):

```diff
- import { useRoute, Route, Switch } from "wouter";
+ import { useRoute, Route, Switch } from "wouter-preact";
```

You might need to ensure you have the latest version of
[Preact X](https://github.com/preactjs/preact/releases/tag/10.0.0-alpha.0) with support for hooks.

**[▶ Demo Sandbox](https://codesandbox.io/s/wouter-preact-0lr3n)**

### Server-side Rendering support (SSR)?

In order to render your app on the server, you'll need to wrap your app with top-level Router and
specify `ssrPath` prop (usually, derived from current request). Optionally, `Router` accepts `ssrSearch` parameter if need to have access to a search string on a server.

```js
import { renderToString } from "react-dom/server";
import { Router } from "wouter";

const handleRequest = (req, res) => {
  // top-level Router is mandatory in SSR mode
  // pass an optional context object to handle redirects on the server
  const ssrContext = {};
  const prerendered = renderToString(
    <Router ssrPath={req.path} ssrSearch={req.search} ssrContext={ssrContext}>
      <App />
    </Router>
  );

  if (ssrContext.redirectTo) {
    // encountered redirect
    res.redirect(ssrContext.redirectTo);
  } else {
    // respond with prerendered html
  }
};
```

Tip: wouter can pre-fill `ssrSearch`, if `ssrPath` contains the `?` character. So these are equivalent:

```jsx
<Router ssrPath="/goods?sort=asc" />;

// is the same as
<Router ssrPath="/goods" ssrSearch="sort=asc" />;
```

On the client, the static markup must be hydrated in order for your app to become interactive. Note
that to avoid having hydration warnings, the JSX rendered on the client must match the one used by
the server, so the `Router` component must be present.

```js
import { hydrateRoot } from "react-dom/client";

const root = hydrateRoot(
  domNode,
  // during hydration, `ssrPath` is set to `location.pathname`,
  // `ssrSearch` set to `location.search` accordingly
  // so there is no need to explicitly specify them
  <Router>
    <App />
  </Router>
);
```

**[▶ Demo](https://github.com/molefrog/wultra)**

### How do I configure the router to render a specific route in tests?

Testing with wouter is no different from testing regular React apps. You often need a way to provide a fixture for the current location to render a specific route. This can be easily done by swapping the normal location hook with `memoryLocation`. It is an initializer function that returns a hook that you can then specify in a top-level `Router`.

```jsx
import { render } from "@testing-library/react";
import { memoryLocation } from "wouter/memory-location";

it("renders a user page", () => {
  // `static` option makes it immutable
  // even if you call `navigate` somewhere in the app location won't change
  const { hook, searchHook } = memoryLocation({ path: "/user/2", static: true });

  const { container } = render(
    <Router hook={hook} searchHook={searchHook}>
      <Route path="/user/:id">{(params) => <>User ID: {params.id}</>}</Route>
    </Router>
  );

  expect(container.innerHTML).toBe("User ID: 2");
});
```

**Note:** When you pass a `hook` prop to `Router`, it will automatically inherit the `searchHook` from the hook if available (via `hook.searchHook`). This means you don't need to explicitly pass both `hook` and `searchHook` when using `memoryLocation` - just passing `hook` is enough for `useSearch()` to work correctly with query parameters.

```jsx
it("works with query parameters", () => {
  const { hook } = memoryLocation({ path: "/products?sort=price&order=asc" });

  const { result } = renderHook(() => useSearch(), {
    wrapper: ({ children }) => <Router hook={hook}>{children}</Router>,
  });

  expect(result.current).toBe("sort=price&order=asc");
});
```

The hook can be configured to record navigation history. Additionally, it comes with a `navigate` function for external navigation.

```jsx
it("performs a redirect", () => {
  const { hook, history, navigate } = memoryLocation({
    path: "/",
    // will store navigation history in `history`
    record: true,
  });

  const { container } = render(
    <Router hook={hook}>
      <Switch>
        <Route path="/">Index</Route>
        <Route path="/orders">Orders</Route>

        <Route>
          <Redirect to="/orders" />
        </Route>
      </Switch>
    </Router>
  );

  expect(history).toStrictEqual(["/"]);

  navigate("/unknown/route");

  expect(container.innerHTML).toBe("Orders");
  expect(history).toStrictEqual(["/", "/unknown/route", "/orders"]);
});
```

### 1KB is too much, I can't afford it!

We've got some great news for you! If you're a minimalist bundle-size nomad and you need a damn
simple routing in your app, you can just use bare location hooks. For example, `useBrowserLocation` hook which is only **650 bytes gzipped**
and manually match the current location with it:

```js
import { useBrowserLocation } from "wouter/use-browser-location";

const UsersRoute = () => {
  const [location] = useBrowserLocation();

  if (location !== "/users") return null;

  // render the route
};
```

Wouter's motto is **"Minimalist-friendly"**.

## Contributing

**Architecture principles:**

- All code is written in JavaScript for full control over size optimization
- TypeScript definitions are maintained separately in `types/` directories
- `wouter-preact` reuses the same source except for `react-deps.js` (Preact-specific hooks)
- Type definitions are duplicated between packages (not ideal, but works for now)

**Development:** Tests run directly from source files (no build required). Run `npm run test` for interactive mode or `npm run test -- --run` for a single run. Use `npm run build` to build the distributable package before publishing.

## Acknowledgements

Wouter illustrations and logos were made by [Katya Simacheva](https://simachevakatya.com/) and
[Katya Vakulenko](https://katyavakulenko.com/). Thank you to **[@jeetiss](https://github.com/jeetiss)**
and all the amazing [contributors](https://github.com/molefrog/wouter/graphs/contributors) for
helping with the development.


================================================
FILE: bunfig.toml
================================================
[test]
preload = ["./packages/wouter/test/setup.ts"]
coverageSkipTestFiles = true
coveragePathIgnorePatterns = ["**/test/**"]


================================================
FILE: package.json
================================================
{
  "name": "monorepo",
  "private": true,
  "description": "A minimalistic routing for React and Preact. Monorepo package.",
  "type": "module",
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "fix:p": "prettier --write \"./**/*.(js|ts){x,}\"",
    "test": "bun test",
    "test-types": "tsc --noEmit",
    "size": "size-limit",
    "lint": "eslint packages/**/*.js",
    "build": ":"
  },
  "author": "Alexey Taktarov <molefrog@gmail.com>",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/molefrog/wouter.git"
  },
  "license": "ISC",
  "sideEffects": false,
  "prettier": {
    "tabWidth": 2,
    "semi": true,
    "singleQuote": false,
    "printWidth": 80
  },
  "size-limit": [
    {
      "path": "packages/wouter/src/index.js",
      "limit": "2500 B",
      "ignore": [
        "react",
        "use-sync-external-store"
      ]
    },
    {
      "path": "packages/wouter/src/use-browser-location.js",
      "limit": "1000 B",
      "import": "{ useBrowserLocation }",
      "ignore": [
        "react",
        "use-sync-external-store"
      ]
    },
    {
      "path": "packages/wouter/src/memory-location.js",
      "limit": "1000 B",
      "ignore": [
        "react",
        "use-sync-external-store"
      ]
    },
    {
      "path": "packages/wouter/src/use-hash-location.js",
      "limit": "1000 B",
      "ignore": [
        "react",
        "use-sync-external-store"
      ]
    },
    {
      "path": "packages/wouter-preact/src/index.js",
      "limit": "2500 B",
      "ignore": [
        "preact",
        "preact/hooks"
      ]
    },
    {
      "path": "packages/wouter-preact/src/use-browser-location.js",
      "limit": "1000 B",
      "import": "{ useBrowserLocation }",
      "ignore": [
        "preact",
        "preact/hooks"
      ]
    },
    {
      "path": "packages/wouter-preact/src/use-hash-location.js",
      "limit": "1000 B",
      "ignore": [
        "preact",
        "preact/hooks"
      ]
    },
    {
      "path": "packages/wouter-preact/src/memory-location.js",
      "limit": "1000 B",
      "ignore": [
        "preact",
        "preact/hooks"
      ]
    }
  ],
  "husky": {
    "hooks": {
      "commit-msg": "npm run fix:p"
    }
  },
  "eslintConfig": {
    "extends": "eslint:recommended",
    "parserOptions": {
      "sourceType": "module",
      "ecmaFeatures": {
        "jsx": true
      }
    },
    "env": {
      "es2020": true,
      "browser": true,
      "node": true
    },
    "rules": {
      "no-unused-vars": [
        "error",
        {
          "varsIgnorePattern": "^_",
          "argsIgnorePattern": "^_"
        }
      ],
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn"
    },
    "plugins": [
      "react-hooks"
    ],
    "ignorePatterns": [
      "types/**"
    ]
  },
  "devDependencies": {
    "@happy-dom/global-registrator": "^20.0.10",
    "@size-limit/preset-small-lib": "^11.2.0",
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.1.4",
    "@testing-library/react": "^16.3.0",
    "@types/bun": "1.3.3",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "copyfiles": "^2.4.1",
    "eslint": "^7.19.0",
    "eslint-plugin-react-hooks": "^4.6.2",
    "happy-dom": "^20.0.10",
    "husky": "^4.3.0",
    "path-to-regexp": "^6.2.1",
    "preact": "^10.23.2",
    "preact-render-to-string": "^6.5.9",
    "prettier": "^2.4.1",
    "react": "^19",
    "react-dom": "^19",
    "size-limit": "^11.2.0",
    "typescript": "^5.8.0"
  }
}


================================================
FILE: packages/magazin/.gitignore
================================================
# dependencies (bun install)
node_modules

# output
out
dist
*.tgz

# code coverage
coverage
*.lcov

# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store


================================================
FILE: packages/magazin/App.tsx
================================================
import { Route, Switch, Redirect } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";
import { HomePage } from "@/routes/home.tsx";
import { AboutPage } from "@/routes/about.tsx";
import { NotFoundPage } from "@/routes/404.tsx";
import { ProductPage } from "@/routes/products/[slug].tsx";
import { CartPage } from "@/routes/cart.tsx";
import { WithStatusCode } from "@/components/with-status-code.tsx";
import { Navbar } from "@/components/navbar.tsx";
import { Footer } from "@/components/footer.tsx";

export function App() {
  return (
    <div className="min-h-screen bg-white flex flex-col">
      <Helmet titleTemplate="%s · Magazin by wouter" />

      <Navbar />

      <div className="min-h-[85vh] pt-12">
        <main className="max-w-4xl mx-auto px-6 py-28">
          <Switch>
            <Route path="/">
              <HomePage />
            </Route>

            <Route path="/about">
              <AboutPage />
            </Route>

            <Route path="/products/:slug">
              {(params) => <ProductPage slug={params.slug} />}
            </Route>

            <Route path="/cart">
              <CartPage />
            </Route>

            <Route path="/featured">
              <Redirect to="/products/hook-keyring-rvst" />
            </Route>

            <Route>
              <WithStatusCode code={404}>
                <NotFoundPage />
              </WithStatusCode>
            </Route>
          </Switch>
        </main>
      </div>

      <Footer />
    </div>
  );
}


================================================
FILE: packages/magazin/client.tsx
================================================
import { hydrateRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { Router, type AroundNavHandler } from "wouter";
import { HelmetProvider } from "@dr.pogodin/react-helmet";
import { App } from "./App";

// Enable view transitions for navigation
const aroundNav: AroundNavHandler = (navigate, to, options) => {
  // Feature detection for browsers that don't support View Transitions
  if (!document.startViewTransition) {
    navigate(to, options);
    return;
  }

  // Only use view transitions if explicitly requested
  if (options?.transition) {
    document.startViewTransition(() => {
      flushSync(() => {
        navigate(to, options);
      });
    });
  } else {
    navigate(to, options);
  }
};

hydrateRoot(
  document.body,
  <HelmetProvider>
    <Router aroundNav={aroundNav}>
      <App />
    </Router>
  </HelmetProvider>
);


================================================
FILE: packages/magazin/components/footer.tsx
================================================
import { Link } from "wouter";

export function Footer() {
  return (
    <div className="max-w-4xl w-full px-6 self-center">
      <footer className="max-w-4xl mx-auto pt-12 pb-20 border-t border-gray-100">
        <div className="w-full md:flex md:justify-between gap-8">
          <div className="space-y-3 flex-1">
            <div className="flex items-center gap-2">
              <span className="text-base font-medium text-neutral-700">
                magazin
              </span>
            </div>
            <p className="text-sm text-neutral-400 text-pretty">
              A modern e-commerce demo built with wouter and Bun.
              <br /> No rights reserved.
            </p>
          </div>

          <div className="pt-6">
            <a
              href="https://github.com/molefrog/wouter/tree/v3/packages/magazin"
              className="inline-flex items-center gap-2 px-3 py-1 border border-neutral-200 rounded-lg hover:bg-neutral-50 transition-colors"
            >
              <i className="iconoir-git-fork text-base" />
              <span className="text-sm font-medium text-gray-900">
                View Source
              </span>
            </a>
          </div>
        </div>
      </footer>
    </div>
  );
}


================================================
FILE: packages/magazin/components/navbar.tsx
================================================
import { Link } from "wouter";

function Logo() {
  return <i className="iconoir-spark-solid text-2xl text-indigo-500" />;
}

function NavLink({
  href,
  children,
}: {
  href: string;
  children: React.ReactNode;
}) {
  return (
    <Link
      href={href}
      transition
      className={(active) =>
        `text-sm font-medium ${
          active ? "text-gray-900" : "text-gray-500 hover:text-gray-900"
        }`
      }
    >
      {children}
    </Link>
  );
}

export function Navbar() {
  return (
    <nav className="fixed top-0 left-0 right-0 z-50 border-b border-gray-200 bg-white py-1.5">
      <div className="max-w-4xl mx-auto flex items-center justify-between px-6">
        <Link
          href="/"
          transition
          className="flex items-center gap-2 hover:bg-neutral-200/50 rounded-md p-1"
        >
          <Logo />
        </Link>

        <div className="flex items-center gap-8">
          <NavLink href="/">Home</NavLink>
          <NavLink href="/about">About</NavLink>
        </div>

        <Link
          href="/cart"
          transition
          className="relative flex items-center hover:bg-neutral-200/50 rounded-md p-1"
        >
          <i className="iconoir-cart text-xl" />
          <span className="absolute -top-1.5 -right-2 flex h-4 w-4 items-center justify-center rounded-full bg-gray-900 text-[10px] font-semibold text-white">
            7
          </span>
        </Link>
      </div>
    </nav>
  );
}


================================================
FILE: packages/magazin/components/star-wouter.tsx
================================================
import { useState, useEffect } from "react";

export function StarWouter() {
  const [stars, setStars] = useState<number | null>(null);

  useEffect(() => {
    fetch("https://api.github.com/repos/molefrog/wouter")
      .then((res) => res.json())
      .then((data) => {
        if (data.stargazers_count) {
          setStars(data.stargazers_count);
        }
      })
      .catch(() => {
        // If fetch fails, we just won't show the count
        setStars(null);
      });
  }, []);

  return (
    <a
      href="https://github.com/molefrog/wouter"
      className="inline-flex items-center gap-2 px-3 h-9 border border-neutral-200 rounded-xl hover:bg-neutral-50 transition-colors select-none whitespace-nowrap flex-shrink-0"
    >
      <i className="iconoir-star-solid text-base text-yellow-500" />
      <span className="text-sm font-medium">Star Wouter</span>
      <div className="bg-neutral-200 w-px h-3.5 mx-1 self-center"></div>
      <div className="flex items-center gap-1.5">
        <span className="text-sm text-gray-900">
          {stars !== null ? stars.toLocaleString() : "\u221E"}
        </span>
      </div>
    </a>
  );
}


================================================
FILE: packages/magazin/components/with-status-code.tsx
================================================
import { useRouter } from "wouter";

export function WithStatusCode({
  code,
  children,
}: {
  code: number;
  children: React.ReactNode;
}) {
  const router = useRouter();

  // Set status code on SSR context if available
  if (router.ssrContext) {
    router.ssrContext.statusCode = code;
  }

  return <>{children}</>;
}


================================================
FILE: packages/magazin/db/products.ts
================================================
export interface Product {
  slug: string;
  name: string;
  price: number;
  brand: string;
  category: string;
  image: string;
  description: string;
}

export const products: Product[] = [
  {
    image: "/products/carabiner.webp",
    slug: "hook-keyring-rvst",
    brand: "RVST",
    category: "Accessories",
    name: "Hook Keyring",
    price: 65,
    description:
      "Premium carabiner keyring crafted with attention to detail and designed for everyday carry.",
  },
  {
    image: "/products/ring.webp",
    slug: "silver-ok-ring",
    brand: "Rick Woens",
    category: "Jewelry",
    name: "Silver OK Ring",
    price: 99,
    description:
      "Handcrafted sterling silver ring with a unique OK gesture design.",
  },
  {
    image: "/products/navigator-cap.webp",
    slug: "navigator-baseball-cap",
    brand: "Rendr",
    category: "Accessories",
    name: "Navigator Baseball Cap",
    price: 179,
    description:
      "Premium baseball cap with embroidered branding and adjustable fit.",
  },
  {
    image: "/products/sizelimited-tshirt.webp",
    slug: "size-limited-tshirt",
    brand: "Rendr",
    category: "Clothing",
    name: "Size Limited T-Shirt",
    price: 65,
    description:
      "Comfortable cotton t-shirt with minimalist branding and premium fit.",
  },
  {
    image: "/products/wouter-glasses.webp",
    slug: "wouter-cult-glasses",
    brand: "Wouter",
    category: "Accessories",
    name: "Wouter Glasses",
    price: 129,
    description:
      "Cult glasses worn by wouter. Minimalist design with premium frames and crystal-clear lenses.",
  },
  {
    image: "/products/parka.webp",
    slug: "route-breaker-windbreaker",
    brand: "Wouter",
    category: "Clothing",
    name: "Route Breaker Windbreaker",
    price: 249,
    description:
      "Navigate any weather with the Route Breaker. Lightweight, water-resistant, and built for those who hook into adventure.",
  },
  {
    image: "/products/react-pendant.webp",
    slug: "react-state-pendant",
    brand: "Wouter",
    category: "Jewelry",
    name: "React State Pendant",
    price: 159,
    description:
      "A declarative pendant for those who embrace the component lifecycle. Hooks perfectly with any chain.",
  },
  {
    image: "/products/scarf.webp",
    slug: "nested-routes-silk-scarf",
    brand: "Wouter",
    category: "Accessories",
    name: "Nested Routes Silk Scarf",
    price: 189,
    description:
      "Luxurious silk scarf featuring an intricate wouter pattern. Each layer wraps seamlessly into the next, just like your favorite routes.",
  },
  {
    image: "/products/poster-a.webp",
    slug: "keep-routing-poster",
    brand: "Wouter",
    category: "Art",
    name: "Keep Routing Poster",
    price: 45,
    description:
      "Minimalist poster with a bold message for developers. Museum-quality print that reminds you to stay on the path.",
  },
];

export function getProductBySlug(slug: string): Product | undefined {
  return products.find((p) => p.slug === slug);
}


================================================
FILE: packages/magazin/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/webp" href="/public/favicon.webp" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css"
    />
    <link rel="stylesheet" href="./styles.css" />
    <title><!-- injected during SSR --></title>
    <script src="./client.tsx" type="module"></script>
  </head>
  <body></body>
</html>


================================================
FILE: packages/magazin/index.tsx
================================================
import { renderToReadableStream } from "react-dom/server";
import { Router } from "wouter";
import {
  HelmetProvider,
  type HelmetDataContext,
} from "@dr.pogodin/react-helmet";
import { App } from "./App.tsx";
import tailwind from "bun-plugin-tailwind";

// Build the HTML and all its assets before starting the server
const isProduction = process.env.NODE_ENV === "production";

const build = await Bun.build({
  entrypoints: ["./index.html"],
  // No outdir = files are kept in memory, not written to disk
  minify: isProduction,
  publicPath: "/",
  sourcemap: "linked",
  plugins: [tailwind],
  define: {
    "process.env.NODE_ENV": JSON.stringify(
      process.env.NODE_ENV || "development"
    ),
  },
});

if (!build.success) {
  console.error("Build failed:", build.logs);
  process.exit(1);
}

// Create a map of assets by their path for quick lookup
const assets = new Map<string, (typeof build.outputs)[number]>();
let htmlTemplate: string | null = null;

for (const output of build.outputs) {
  // The HTML file will be used as template for SSR
  if (output.path.endsWith(".html")) {
    htmlTemplate = await output.text();
  } else {
    // Store other assets (JS, CSS, etc.) by their basename
    const basename = "/" + output.path.split("/").pop()!;
    assets.set(basename, output);
  }
}

if (!htmlTemplate) {
  console.error("No HTML template found in build outputs");
  process.exit(1);
}

const port = process.env.PORT ? parseInt(process.env.PORT) : 3002;

Bun.serve({
  port,
  async fetch(req) {
    const url = new URL(req.url);

    // Check if this is a request for a built asset
    const asset = assets.get(url.pathname);
    if (asset) {
      const headers = isProduction
        ? {
            // Built assets have content hashes, so they can be cached indefinitely
            "Cache-Control": "public, max-age=31536000, immutable",
          }
        : {};

      return new Response(asset, { headers });
    }

    // Check if this is a request for a static file from public/
    const publicFile = Bun.file(`./public${url.pathname}`);
    if (await publicFile.exists()) {
      const headers = new Headers();

      // Add 24h caching for static assets in production
      if (isProduction) {
        headers.set("Cache-Control", "public, max-age=86400");
      }

      return new Response(publicFile, { headers });
    }

    // Otherwise, it's a page request - render with SSR
    // ssrPath accepts full path with search, e.g. "/foo?bar=1"
    // ssrContext is used to handle redirects and status codes on the server
    const ssrContext: { redirectTo?: string; statusCode?: number } = {};
    const helmetContext: HelmetDataContext = {};

    const stream = await renderToReadableStream(
      <HelmetProvider context={helmetContext}>
        <Router ssrPath={url.pathname + url.search} ssrContext={ssrContext}>
          <App />
        </Router>
      </HelmetProvider>
    );

    // Check if a redirect occurred during SSR
    if (ssrContext.redirectTo) {
      return Response.redirect(
        new URL(ssrContext.redirectTo, url.origin).toString(),
        302
      );
    }

    // Get status code from context, default to 200
    const statusCode = ssrContext.statusCode || 200;

    // Convert stream to string
    const appHtml = await new Response(stream).text();

    const helmet = helmetContext.helmet;

    // Use HTMLRewriter to inject the SSR content into body and title
    const rewriter = new HTMLRewriter()
      .on("body", {
        element(element) {
          element.setInnerContent(appHtml, { html: true });
        },
      })
      .on("title", {
        element(element) {
          if (!helmet) return;
          // Remove the existing title tag and let helmet's title be appended to head
          element.remove();
        },
      })
      .on("head", {
        element(element) {
          if (!helmet) return;

          const headContent = [
            helmet.title?.toString(),
            helmet.priority?.toString(),
            helmet.meta?.toString(),
            helmet.link?.toString(),
            helmet.script?.toString(),
          ]
            .filter(Boolean)
            .join("\n");

          if (headContent) {
            element.append(headContent, { html: true });
          }
        },
      });

    const transformedResponse = rewriter.transform(new Response(htmlTemplate));

    return new Response(transformedResponse.body, {
      status: statusCode,
      headers: { "Content-Type": "text/html" },
    });
  },
});

console.log(`Server running at http://localhost:${port}`);


================================================
FILE: packages/magazin/package.json
================================================
{
  "name": "magazin",
  "module": "index.ts",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "bun --watch index.tsx",
    "start": "bun index.tsx",
    "prod": "NODE_ENV=production bun index.tsx"
  },
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5"
  },
  "dependencies": {
    "@dr.pogodin/react-helmet": "^3.0.4",
    "bun-plugin-tailwind": "^0.1.2",
    "tailwindcss": "^4.1.17",
    "wouter": "workspace:*"
  }
}


================================================
FILE: packages/magazin/routes/404.tsx
================================================
import { Link } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";

export function NotFoundPage() {
  return (
    <div className="pt-12">
      <Helmet>
        <title>Page Not Found</title>
      </Helmet>
      <div className="flex flex-col items-center justify-center  border border-neutral-300/75 rounded-lg p-4 w-9 h-9 font-medium text-neutral-400 mx-auto text-xs mb-6">
        404
      </div>

      <h1 className="text-3xl font-medium tracking-tight text-center mb-4">
        Not Found
      </h1>
      <p className="text-neutral-400 text-center max-w-md mx-auto">
        We are sorry, but the page you're looking for doesn't exist. Try going
        back to the{" "}
        <Link href="/" className="underline hover:text-neutral-900">
          home page
        </Link>
        .
      </p>
    </div>
  );
}


================================================
FILE: packages/magazin/routes/about.tsx
================================================
import { Link } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";

function Feature({ children }: { children: React.ReactNode }) {
  return (
    <li className="text-neutral-800 flex items-center gap-3">
      <i className="iconoir-cable-tag-solid text-blue-500 text-xl" />
      {children}
    </li>
  );
}

export function AboutPage() {
  return (
    <>
      <Helmet>
        <title>About</title>
      </Helmet>

      <h1 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-2">
        What is this?
      </h1>
      <p className="text-neutral-500 max-w-lg mb-16">
        This is a simple SSR demo showcasing wouter v3.9.0, React 19 with
        server-side rendering and client-side hydration running on Bun.
      </p>

      <div className="text-sm text-neutral-400 mb-2">Features</div>
      <ul className="mt-4 list-inside list-nonespace-y-1 grid grid-cols-1 sm:grid-cols-2 auto-rows-fr gap-3.5">
        <Feature>
          <Link href="/products/hook-keyring-rvst" className="hover:underline">
            Dynamic segments
          </Link>
        </Feature>
        <Feature>
          <Link href="/this-page-does-not-exist" className="hover:underline">
            Default switch route (404)
          </Link>
        </Feature>
        <Feature>
          <Link
            href="/?category=accessories&sort=price-desc"
            className="hover:underline"
          >
            Search parameters
          </Link>
        </Feature>
        <Feature>
          <Link href="/featured" className="hover:underline">
            Redirect with SSR support
          </Link>
        </Feature>
        <Feature>
          <Link href="/about" className="hover:underline">
            Active links
          </Link>
        </Feature>
        <Feature>
          <Link
            href="/cart"
            state={{ addedItem: "Demo Product" }}
            className="hover:underline"
          >
            Navigation with state
          </Link>
        </Feature>
        <Feature>
          <Link href="/this-page-does-not-exist" className="hover:underline">
            Custom status codes (404)
          </Link>
        </Feature>
        <Feature>
          <Link href="/" className="hover:underline" transition>
            View transitions
          </Link>
        </Feature>
      </ul>
    </>
  );
}


================================================
FILE: packages/magazin/routes/cart.tsx
================================================
import { useEffect, useState } from "react";
import { useLocation } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";
import { products, type Product } from "@/db/products";

const cartItems: Array<{ product: Product; quantity: number }> = [
  { product: products[4]!, quantity: 1 }, // Wouter Glasses
  { product: products[5]!, quantity: 1 }, // Route Breaker Windbreaker
  { product: products[0]!, quantity: 2 }, // Hook Keyring
  { product: products[7]!, quantity: 3 }, // Keep Routing Poster
];

function NotificationBanner({
  show,
  message,
}: {
  show: boolean;
  message: string | null;
}) {
  if (!message) return null;

  return (
    <div
      className={`fixed bottom-10 left-1/2 -translate-x-1/2 bg-white border border-neutral-200 text-neutral-900 px-3.5 py-2 rounded-lg shadow-lg transition-transform duration-300 starting:translate-y-32 ${
        show ? "translate-y-0" : "translate-y-32"
      }`}
    >
      <div className="flex items-center gap-2">
        <i className="iconoir-shopping-bag-plus text-lg" />
        <span className="text-sm font-medium">{message} added to cart</span>
      </div>
    </div>
  );
}

export function CartPage() {
  const [location, navigate] = useLocation();
  const [showNotification, setShowNotification] = useState(false);
  const [addedItem, setAddedItem] = useState<string | null>(null);

  useEffect(() => {
    const state = history.state as { addedItem?: string } | null;
    if (state?.addedItem) {
      setAddedItem(state.addedItem);
      setShowNotification(true);

      // Clear the state so it doesn't show again on refresh
      navigate(location, { replace: true, state: null });

      // Hide notification after 3 seconds
      const timer = setTimeout(() => {
        setShowNotification(false);
      }, 3000);

      return () => clearTimeout(timer);
    }
  }, [location, navigate]);

  return (
    <>
      <Helmet>
        <title>Cart</title>
      </Helmet>

      <h1 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-6">
        Shopping Cart
      </h1>

      <div className="space-y-3">
        {cartItems.map((item, index) => (
          <div
            key={index}
            className="flex items-start justify-between border-b border-gray-100 pb-4"
          >
            <div className="flex gap-4">
              <div className="w-12 h-12 bg-stone-100 rounded-md shrink-0 p-2">
                <img
                  src={item.product.image}
                  alt={item.product.name}
                  className="w-full h-full object-cover"
                />
              </div>
              <div className="flex flex-col">
                <span className="text-neutral-900">{item.product.name}</span>
                <span className="text-sm text-gray-500 mt-1">
                  {item.quantity} × ${item.product.price}
                </span>
              </div>
            </div>
            <span className="text-neutral-900 text-sm">
              ${item.product.price}
            </span>
          </div>
        ))}
      </div>

      <div className="text-right mt-4">
        <div className="text-sm text-right text-neutral-500">Total</div>
        <div className="text-base font-semibold text-neutral-900">$643</div>
      </div>

      <NotificationBanner show={showNotification} message={addedItem} />
    </>
  );
}


================================================
FILE: packages/magazin/routes/home.tsx
================================================
import { useSearchParams, Link } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";
import { products, type Product } from "@/db/products";
import { StarWouter } from "@/components/star-wouter";

function ProductCard({ slug, brand, category, name, price, image }: Product) {
  return (
    <Link
      href={`/products/${slug}`}
      transition
      className="overflow-hidden group flex flex-col h-full"
    >
      <div
        className="w-full aspect-square p-12 bg-stone-100/75 group-hover:bg-stone-200/75 transition-colors rounded-t-lg"
        style={{ viewTransitionName: `product-image-${slug}` }}
      >
        <img src={image} alt={name} className="object-contain w-full h-full" />
      </div>
      <div className="p-4 bg-stone-100/75 rounded-b-lg group-hover:bg-stone-200/75 transition-colors flex-1 flex flex-col justify-between">
        <div className="text-sm text-neutral-400/75">
          {brand} · {category}
        </div>
        <div className="mt-1 flex items-center justify-between">
          <span className="font-medium text-sm">{name}</span>
          <span className="">${price.toLocaleString()}</span>
        </div>
      </div>
    </Link>
  );
}

const categories = [
  { value: "all", label: "All" },
  { value: "accessories", label: "Accessories" },
  { value: "clothing", label: "Clothing" },
  { value: "jewelry", label: "Jewelry" },
  { value: "art", label: "Art" },
];

const sortOptions = [
  { value: "newest", label: "Newest" },
  { value: "price-asc", label: "Price: Low to High" },
  { value: "price-desc", label: "Price: High to Low" },
  { value: "name", label: "Name" },
];

function CategoryFilter({
  value,
  onChange,
}: {
  value: string;
  onChange: (value: string) => void;
}) {
  return (
    <div className="flex items-center gap-4">
      {categories.map((cat) => (
        <button
          key={cat.value}
          onClick={() => onChange(cat.value)}
          className={`text-sm cursor-pointer ${
            value === cat.value
              ? "text-neutral-900 underline underline-offset-4"
              : "text-neutral-500 hover:text-neutral-900"
          }`}
        >
          {cat.label}
        </button>
      ))}
    </div>
  );
}

function SortSelect({
  value,
  onChange,
}: {
  value: string;
  onChange: (value: string) => void;
}) {
  return (
    <div className="relative flex md:inline-flex items-center cursor-pointer w-full md:w-auto">
      <select
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="appearance-none bg-transparent text-sm text-neutral-500 pr-4 cursor-pointer hover:text-neutral-900 focus:outline-none text-left md:text-right w-full md:w-auto"
      >
        {sortOptions.map((opt) => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
      <i className="iconoir-nav-arrow-down absolute right-0 text-xs pointer-events-none text-neutral-500 cursor-pointer ml-1" />
    </div>
  );
}

export function HomePage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get("category") || "all";
  const sort = searchParams.get("sort") || "newest";

  const handleFilterChange = (key: string, value: string) => {
    setSearchParams((params) => {
      const newParams = new URLSearchParams(params);
      if (value === "all" || value === "newest") {
        newParams.delete(key);
      } else {
        newParams.set(key, value);
      }
      return newParams;
    });
  };

  // Filter products by category
  let filteredProducts = products;
  if (category !== "all") {
    filteredProducts = products.filter(
      (p) => p.category.toLowerCase() === category.toLowerCase()
    );
  }

  // Sort products
  const sortedProducts = [...filteredProducts].sort((a, b) => {
    switch (sort) {
      case "price-asc":
        return a.price - b.price;
      case "price-desc":
        return b.price - a.price;
      case "name":
        return a.name.localeCompare(b.name);
      case "newest":
      default:
        return 0; // Keep original order
    }
  });

  return (
    <>
      <Helmet>
        <title>Magazin by wouter</title>
      </Helmet>

      <div className="mb-20">
        <h1 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-2">
          Welcome to our shop
        </h1>
        <p className="text-lg text-neutral-500 mb-4 max-w-2xl text-pretty">
          Exclusive merch for hardcore wouter fans. You can't buy these yet, so
          go star the repo to increase our chances of becoming a billion dollar
          company.
        </p>
        <div className="flex items-center gap-3 overflow-x-auto flex-nowrap -mx-6 px-6 md:mx-0 md:px-0">
          <button
            onClick={() =>
              document
                .getElementById("products")
                ?.scrollIntoView({ behavior: "smooth" })
            }
            className="bg-black text-white px-3 text-sm font-medium py-2 rounded-xl hover:bg-neutral-800 transition-colors shadow-sm cursor-pointer whitespace-nowrap flex-shrink-0"
          >
            Start shopping →
          </button>
          <StarWouter />
        </div>
      </div>

      <div
        className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 py-6 scroll-mt-16"
        id="products"
      >
        <CategoryFilter
          value={category}
          onChange={(v) => handleFilterChange("category", v)}
        />
        <SortSelect
          value={sort}
          onChange={(v) => handleFilterChange("sort", v)}
        />
      </div>

      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 auto-rows-max gap-2.5">
        {sortedProducts.map((product) => (
          <ProductCard key={product.slug} {...product} />
        ))}
      </div>

      {sortedProducts.length === 0 && (
        <div className="text-center py-12 text-neutral-500">
          No products found in this category.
        </div>
      )}
    </>
  );
}


================================================
FILE: packages/magazin/routes/products/[slug].tsx
================================================
import { Link } from "wouter";
import { Helmet } from "@dr.pogodin/react-helmet";
import { getProductBySlug } from "@/db/products";

export function ProductPage({ slug }: { slug: string }) {
  const product = getProductBySlug(slug);

  if (!product) {
    return (
      <div className="text-center py-12">
        <Helmet>
          <title>Product Not Found</title>
        </Helmet>
        <h1 className="text-2xl font-semibold text-neutral-900 mb-2">
          Product not found
        </h1>
        <Link href="/" className="text-sm text-neutral-500 hover:underline">
          Back to home
        </Link>
      </div>
    );
  }

  return (
    <>
      <Helmet>
        <title>{product.name}</title>
      </Helmet>
      <Link
        href="/"
        transition
        className=" inline-flex items-center gap-2  hover:bg-neutral-100/75 rounded-md p-1.5 hover:text-neutral-900 mb-2"
      >
        <i className="iconoir-reply text-base" />
      </Link>
      <div className="grid grid-cols-2 md:grid-cols-3 md:gap-12 gap-6">
        <div
          className="bg-stone-100/75 rounded-lg aspect-square md:col-span-2 p-12"
          style={{ viewTransitionName: `product-image-${product.slug}` }}
        >
          <img
            src={product.image}
            alt={product.name}
            className="object-contain w-full h-full"
          />
        </div>
        <div className="pt-4">
          <div className="text-sm text-neutral-400 mb-2">
            {product.brand} · {product.category}
          </div>
          <h1 className="text-xl tracking-tight text-neutral-900 mb-2">
            {product.name}
          </h1>
          <p className="text-neutral-500 text-sm">{product.description}</p>

          <div className="mt-4">
            <span className="text-sm">${product.price}</span>
          </div>
          <div className="mt-8">
            <Link
              href="/cart"
              state={{ addedItem: product.name }}
              className="bg-black text-white px-3 text-sm font-medium py-2 rounded-xl hover:bg-neutral-800 transition-colors shadow-sm cursor-pointer w-full inline-block text-center"
            >
              Add to Cart
            </Link>
          </div>
        </div>
      </div>
    </>
  );
}


================================================
FILE: packages/magazin/styles.css
================================================
@import "tailwindcss";

/* View Transitions */
@view-transition {
  navigation: auto;
}

/* Default: simple 0.25s cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.2s;
  animation-timing-function: ease;
}


================================================
FILE: packages/magazin/tsconfig.json
================================================
{
  "compilerOptions": {
    // Environment setup & latest features
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,

    // Bundler mode
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,

    // Path aliases
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    },

    // Best practices
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,

    // Some stricter flags (disabled by default)
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false
  }
}


================================================
FILE: packages/wouter/package.json
================================================
{
  "name": "wouter",
  "version": "3.9.0",
  "description": "Minimalist-friendly ~1.5KB router for React",
  "type": "module",
  "keywords": [
    "react",
    "preact",
    "router",
    "tiny",
    "routing",
    "hooks",
    "useLocation"
  ],
  "files": [
    "src",
    "types/**/*.d.ts",
    "types/*.d.ts"
  ],
  "main": "src/index.js",
  "exports": {
    ".": {
      "types": "./types/index.d.ts",
      "default": "./src/index.js"
    },
    "./use-browser-location": {
      "types": "./types/use-browser-location.d.ts",
      "default": "./src/use-browser-location.js"
    },
    "./use-hash-location": {
      "types": "./types/use-hash-location.d.ts",
      "default": "./src/use-hash-location.js"
    },
    "./memory-location": {
      "types": "./types/memory-location.d.ts",
      "default": "./src/memory-location.js"
    }
  },
  "types": "types/index.d.ts",
  "typesVersions": {
    ">=4.1": {
      "types/index.d.ts": [
        "types/index.d.ts"
      ],
      "use-browser-location": [
        "types/use-browser-location.d.ts"
      ],
      "use-hash-location": [
        "types/use-hash-location.d.ts"
      ],
      "memory-location": [
        "types/memory-location.d.ts"
      ]
    }
  },
  "scripts": {
    "prepublishOnly": "cp ../../README.md ."
  },
  "author": "Alexey Taktarov <molefrog@gmail.com>",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/molefrog/wouter.git"
  },
  "license": "Unlicense",
  "peerDependencies": {
    "react": ">=16.8.0"
  },
  "dependencies": {
    "mitt": "^3.0.1",
    "regexparam": "^3.0.0",
    "use-sync-external-store": "^1.0.0"
  }
}


================================================
FILE: packages/wouter/src/index.d.ts
================================================
export * from "../types/index.js";


================================================
FILE: packages/wouter/src/index.js
================================================
import { parse as parsePattern } from "regexparam";

import {
  useBrowserLocation,
  useSearch as useBrowserSearch,
} from "./use-browser-location.js";

import {
  useRef,
  useContext,
  createContext,
  isValidElement,
  cloneElement,
  createElement as h,
  Fragment,
  forwardRef,
  useIsomorphicLayoutEffect,
  useEvent,
  useMemo,
} from "./react-deps.js";
import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";

/*
 * Router and router context. Router is a lightweight object that represents the current
 * routing options: how location is managed, base path etc.
 *
 * There is a default router present for most of the use cases, however it can be overridden
 * via the <Router /> component.
 */

const defaultRouter = {
  hook: useBrowserLocation,
  searchHook: useBrowserSearch,
  parser: parsePattern,
  base: "",
  // this option is used to override the current location during SSR
  ssrPath: undefined,
  ssrSearch: undefined,
  // optional context to track render state during SSR
  ssrContext: undefined,
  // customizes how `href` props are transformed for <Link />
  hrefs: (x) => x,
  // wraps navigate calls, useful for view transitions
  aroundNav: (n, t, o) => n(t, o),
};

const RouterCtx = createContext(defaultRouter);

// gets the closest parent router from the context
export const useRouter = () => useContext(RouterCtx);

/**
 * Parameters context. Used by `useParams()` to get the
 * matched params from the innermost `Route` component.
 */

const Params0 = {},
  ParamsCtx = createContext(Params0);

export const useParams = () => useContext(ParamsCtx);

/*
 * Part 1, Hooks API: useRoute and useLocation
 */

// Internal version of useLocation to avoid redundant useRouter calls

const useLocationFromRouter = (router) => {
  const [location, navigate] = router.hook(router);

  // the function reference should stay the same between re-renders, so that
  // it can be passed down as an element prop without any performance concerns.
  // (This is achieved via `useEvent`.)
  return [
    relativePath(router.base, location),
    useEvent((to, opts) =>
      router.aroundNav(navigate, absolutePath(to, router.base), opts)
    ),
  ];
};

export const useLocation = () => useLocationFromRouter(useRouter());

export const useSearch = () => {
  const router = useRouter();
  return sanitizeSearch(router.searchHook(router));
};

export const matchRoute = (parser, route, path, loose) => {
  // if the input is a regexp, skip parsing
  const { pattern, keys } =
    route instanceof RegExp
      ? { keys: false, pattern: route }
      : parser(route || "*", loose);

  // array destructuring loses keys, so this is done in two steps
  const result = pattern.exec(path) || [];

  // when parser is in "loose" mode, `$base` is equal to the
  // first part of the route that matches the pattern
  // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)
  // we use this for route nesting
  const [$base, ...matches] = result;

  return $base !== undefined
    ? [
        true,

        (() => {
          // for regex paths, `keys` will always be false

          // an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
          // we "zip" two arrays here to construct the object
          // ["foo"], ["bar"] → { foo: "bar" }
          const groups =
            keys !== false
              ? Object.fromEntries(keys.map((key, i) => [key, matches[i]]))
              : result.groups;

          // convert the array to an instance of object
          // this makes it easier to integrate with the existing param implementation
          let obj = { ...matches };

          // merge named capture groups with matches array
          groups && Object.assign(obj, groups);

          return obj;
        })(),

        // the third value if only present when parser is in "loose" mode,
        // so that we can extract the base path for nested routes
        ...(loose ? [$base] : []),
      ]
    : [false, null];
};

export const useRoute = (pattern) =>
  matchRoute(useRouter().parser, pattern, useLocation()[0]);

/*
 * Part 2, Low Carb Router API: Router, Route, Link, Switch
 */

export const Router = ({ children, ...props }) => {
  // the router we will inherit from - it is the closest router in the tree,
  // unless the custom `hook` is provided (in that case it's the default one)
  const parent_ = useRouter();
  const parent = props.hook ? defaultRouter : parent_;

  // holds to the context value: the router object
  let value = parent;

  // when `ssrPath` contains a `?` character, we can extract the search from it.
  // also, ensure ssrSearch is always defined when ssrPath is provided, so that
  // useSearch behavior matches usePathname (proper SSR hydration when client
  // renders <Router> without props after server rendered with ssrPath/ssrSearch)
  const [path, search = props.ssrSearch ?? ""] =
    props.ssrPath?.split("?") ?? [];
  if (path) (props.ssrSearch = search), (props.ssrPath = path);

  // hooks can define their own `href` formatter (e.g. for hash location)
  props.hrefs = props.hrefs ?? props.hook?.hrefs;

  // hooks can define their own search hook (e.g. for memory location)
  props.searchHook = props.searchHook ?? props.hook?.searchHook;

  // what is happening below: to avoid unnecessary rerenders in child components,
  // we ensure that the router object reference is stable, unless there are any
  // changes that require reload (e.g. `base` prop changes -> all components that
  // get the router from the context should rerender, even if the component is memoized).
  // the expected behaviour is:
  //
  //   1) when the resulted router is no different from the parent, use parent
  //   2) if the custom `hook` prop is provided, we always inherit from the
  //      default router instead. this resets all previously overridden options.
  //   3) when the router is customized here, it should stay stable between renders
  let ref = useRef({}),
    prev = ref.current,
    next = prev;

  for (let k in parent) {
    const option =
      k === "base"
        ? /* base is special case, it is appended to the parent's base */
          parent[k] + (props[k] ?? "")
        : props[k] ?? parent[k];

    if (prev === next && option !== next[k]) {
      ref.current = next = { ...next };
    }

    next[k] = option;

    // the new router is no different from the parent or from the memoized value, use parent
    if (option !== parent[k] || option !== value[k]) value = next;
  }

  return h(RouterCtx.Provider, { value, children });
};

const h_route = ({ children, component }, params) => {
  // React-Router style `component` prop
  if (component) return h(component, { params });

  // support render prop or plain children
  return typeof children === "function" ? children(params) : children;
};

// Cache params object between renders if values are shallow equal
const useCachedParams = (value) => {
  let prev = useRef(Params0);
  const curr = prev.current;
  return (prev.current =
    // Update cache if number of params changed or any value changed
    Object.keys(value).length !== Object.keys(curr).length ||
    Object.entries(value).some(([k, v]) => v !== curr[k])
      ? value // Return new value if there are changes
      : curr); // Return cached value if nothing changed
};

export function useSearchParams() {
  const [location, navigate] = useLocation();

  const search = useSearch();
  const searchParams = useMemo(() => new URLSearchParams(search), [search]);

  // cached value before next render, so you can call setSearchParams multiple times
  let tempSearchParams = searchParams;

  const setSearchParams = useEvent((nextInit, options) => {
    tempSearchParams = new URLSearchParams(
      typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit
    );
    navigate(location + "?" + tempSearchParams, options);
  });

  return [searchParams, setSearchParams];
}

export const Route = ({ path, nest, match, ...renderProps }) => {
  const router = useRouter();
  const [location] = useLocationFromRouter(router);

  const [matches, routeParams, base] =
    // `match` is a special prop to give up control to the parent,
    // it is used by the `Switch` to avoid double matching
    match ?? matchRoute(router.parser, path, location, nest);

  // when `routeParams` is `null` (there was no match), the argument
  // below becomes {...null} = {}, see the Object Spread specs
  // https://tc39.es/proposal-object-rest-spread/#AbstractOperations-CopyDataProperties
  const params = useCachedParams({ ...useParams(), ...routeParams });

  if (!matches) return null;

  const children = base
    ? h(Router, { base }, h_route(renderProps, params))
    : h_route(renderProps, params);

  return h(ParamsCtx.Provider, { value: params, children });
};

export const Link = forwardRef((props, ref) => {
  const router = useRouter();
  const [currentPath, navigate] = useLocationFromRouter(router);

  const {
    to = "",
    href: targetPath = to,
    onClick: _onClick,
    asChild,
    children,
    className: cls,
    /* eslint-disable no-unused-vars */
    replace /* ignore nav props */,
    state /* ignore nav props */,
    transition /* ignore nav props */,
    /* eslint-enable no-unused-vars */

    ...restProps
  } = props;

  const onClick = useEvent((event) => {
    // ignores the navigation when clicked using right mouse button or
    // by holding a special modifier key: ctrl, command, win, alt, shift
    if (
      event.ctrlKey ||
      event.metaKey ||
      event.altKey ||
      event.shiftKey ||
      event.button !== 0
    )
      return;

    _onClick?.(event);
    if (!event.defaultPrevented) {
      event.preventDefault();
      navigate(targetPath, props);
    }
  });

  // handle nested routers and absolute paths
  const href = router.hrefs(
    targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath,
    router // pass router as a second argument for convinience
  );

  return asChild && isValidElement(children)
    ? cloneElement(children, { onClick, href })
    : h("a", {
        ...restProps,
        onClick,
        href,
        // `className` can be a function to apply the class if this link is active
        className: cls?.call ? cls(currentPath === targetPath) : cls,
        children,
        ref,
      });
});

const flattenChildren = (children) =>
  Array.isArray(children)
    ? children.flatMap((c) =>
        flattenChildren(c && c.type === Fragment ? c.props.children : c)
      )
    : [children];

export const Switch = ({ children, location }) => {
  const router = useRouter();
  const [originalLocation] = useLocationFromRouter(router);

  for (const element of flattenChildren(children)) {
    let match = 0;

    if (
      isValidElement(element) &&
      // we don't require an element to be of type Route,
      // but we do require it to contain a truthy `path` prop.
      // this allows to use different components that wrap Route
      // inside of a switch, for example <AnimatedRoute />.
      (match = matchRoute(
        router.parser,
        element.props.path,
        location || originalLocation,
        element.props.nest
      ))[0]
    )
      return cloneElement(element, { match });
  }

  return null;
};

export const Redirect = (props) => {
  const { to, href = to } = props;
  const router = useRouter();
  const [, navigate] = useLocationFromRouter(router);
  const redirect = useEvent(() => navigate(to || href, props));
  const { ssrContext } = router;

  // redirect is guaranteed to be stable since it is returned from useEvent
  useIsomorphicLayoutEffect(() => {
    redirect();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  if (ssrContext) {
    ssrContext.redirectTo = to;
  }

  return null;
};


================================================
FILE: packages/wouter/src/memory-location.d.ts
================================================
export * from "../types/memory-location.js";


================================================
FILE: packages/wouter/src/memory-location.js
================================================
import mitt from "mitt";
import { useSyncExternalStore } from "./react-deps.js";

/**
 * In-memory location that supports navigation
 */

export const memoryLocation = ({
  path = "/",
  searchPath = "",
  static: staticLocation,
  record,
} = {}) => {
  let initialPath = path;
  if (searchPath) {
    // join with & if path contains search query, and ? otherwise
    initialPath += path.split("?")[1] ? "&" : "?";
    initialPath += searchPath;
  }

  let [currentPath, currentSearch = ""] = initialPath.split("?");
  const history = [initialPath];
  const emitter = mitt();

  const navigateImplementation = (path, { replace = false } = {}) => {
    if (record) {
      if (replace) {
        history.splice(history.length - 1, 1, path);
      } else {
        history.push(path);
      }
    }

    [currentPath, currentSearch = ""] = path.split("?");
    emitter.emit("navigate", path);
  };

  const navigate = !staticLocation ? navigateImplementation : () => null;

  const subscribe = (cb) => {
    emitter.on("navigate", cb);
    return () => emitter.off("navigate", cb);
  };

  const useMemoryLocation = () => [
    useSyncExternalStore(subscribe, () => currentPath),
    navigate,
  ];

  const useMemoryQuery = () =>
    useSyncExternalStore(subscribe, () => currentSearch);

  // Attach searchHook to the location hook for auto-inheritance in Router
  useMemoryLocation.searchHook = useMemoryQuery;

  function reset() {
    // clean history array with mutation to preserve link
    history.splice(0, history.length);

    navigateImplementation(initialPath);
  }

  return {
    hook: useMemoryLocation,
    searchHook: useMemoryQuery,
    navigate,
    history: record ? history : undefined,
    reset: record ? reset : undefined,
  };
};


================================================
FILE: packages/wouter/src/paths.js
================================================
/*
 * Transforms `path` into its relative `base` version
 * If base isn't part of the path provided returns absolute path e.g. `~/app`
 */
const _relativePath = (base, path) =>
  !path.toLowerCase().indexOf(base.toLowerCase())
    ? path.slice(base.length) || "/"
    : "~" + path;

/**
 * When basepath is `undefined` or '/' it is ignored (we assume it's empty string)
 */
const baseDefaults = (base = "") => (base === "/" ? "" : base);

export const absolutePath = (to, base) =>
  to[0] === "~" ? to.slice(1) : baseDefaults(base) + to;

export const relativePath = (base = "", path) =>
  _relativePath(unescape(baseDefaults(base)), unescape(path));

/*
 * Removes leading question mark
 */
const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str);

/*
 * decodes escape sequences such as %20
 */
const unescape = (str) => {
  try {
    return decodeURI(str);
  } catch (_e) {
    // fail-safe mode: if string can't be decoded do nothing
    return str;
  }
};

export const sanitizeSearch = (search) => unescape(stripQm(search));


================================================
FILE: packages/wouter/src/react-deps.js
================================================
import * as React from "react";

// React.useInsertionEffect is not available in React <18
// This hack fixes a transpilation issue on some apps
const useBuiltinInsertionEffect = React["useInsertion" + "Effect"];

export {
  useMemo,
  useRef,
  useState,
  useContext,
  createContext,
  isValidElement,
  cloneElement,
  createElement,
  Fragment,
  forwardRef,
} from "react";

// To resolve webpack 5 errors, while not presenting problems for native,
// we copy the approaches from https://github.com/TanStack/query/pull/3561
// and https://github.com/TanStack/query/pull/3601
// ~ Show this aging PR some love to remove the need for this hack:
//   https://github.com/facebook/react/pull/25231 ~
export { useSyncExternalStore } from "./use-sync-external-store.js";

// Copied from:
// https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js
const canUseDOM = !!(
  typeof window !== "undefined" &&
  typeof window.document !== "undefined" &&
  typeof window.document.createElement !== "undefined"
);

// Copied from:
// https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts
// "React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser."
export const useIsomorphicLayoutEffect = canUseDOM
  ? React.useLayoutEffect
  : React.useEffect;

// useInsertionEffect is already a noop on the server.
// See: https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js
export const useInsertionEffect =
  useBuiltinInsertionEffect || useIsomorphicLayoutEffect;

// Userland polyfill while we wait for the forthcoming
// https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
// Note: "A high-fidelity polyfill for useEvent is not possible because
// there is no lifecycle or Hook in React that we can use to switch
// .current at the right timing."
// So we will have to make do with this "close enough" approach for now.
export const useEvent = (fn) => {
  const ref = React.useRef([fn, (...args) => ref[0](...args)]).current;
  // Per Dan Abramov: useInsertionEffect executes marginally closer to the
  // correct timing for ref synchronization than useLayoutEffect on React 18.
  // See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
  useInsertionEffect(() => {
    ref[0] = fn;
  });
  return ref[1];
};


================================================
FILE: packages/wouter/src/use-browser-location.d.ts
================================================
export * from "../types/use-browser-location.js";


================================================
FILE: packages/wouter/src/use-browser-location.js
================================================
import { useSyncExternalStore } from "./react-deps.js";

/**
 * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
 */
const eventPopstate = "popstate";
const eventPushState = "pushState";
const eventReplaceState = "replaceState";
const eventHashchange = "hashchange";
const events = [
  eventPopstate,
  eventPushState,
  eventReplaceState,
  eventHashchange,
];

const subscribeToLocationUpdates = (callback) => {
  for (const event of events) {
    addEventListener(event, callback);
  }
  return () => {
    for (const event of events) {
      removeEventListener(event, callback);
    }
  };
};

export const useLocationProperty = (fn, ssrFn) =>
  useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn);

const currentSearch = () => location.search;

export const useSearch = ({ ssrSearch } = {}) =>
  useLocationProperty(
    currentSearch,
    // != null checks for both null and undefined, but allows empty string ""
    // This allows proper hydration: server renders with ssrSearch="?foo",
    // client hydrates with just <Router /> and reads from location.search
    ssrSearch != null ? () => ssrSearch : currentSearch
  );

const currentPathname = () => location.pathname;

export const usePathname = ({ ssrPath } = {}) =>
  useLocationProperty(
    currentPathname,
    // != null checks for both null and undefined, but allows empty string ""
    // This allows proper hydration: server renders with ssrPath="/foo",
    // client hydrates with just <Router /> and reads from location.pathname
    ssrPath != null ? () => ssrPath : currentPathname
  );

const currentHistoryState = () => history.state;
export const useHistoryState = () =>
  useLocationProperty(currentHistoryState, () => null);

export const navigate = (to, { replace = false, state = null } = {}) =>
  history[replace ? eventReplaceState : eventPushState](state, "", to);

// the 2nd argument of the `useBrowserLocation` return value is a function
// that allows to perform a navigation.
export const useBrowserLocation = (opts = {}) => [usePathname(opts), navigate];

const patchKey = Symbol.for("wouter_v3");

// While History API does have `popstate` event, the only
// proper way to listen to changes via `push/replaceState`
// is to monkey-patch these methods.
//
// See https://stackoverflow.com/a/4585031
if (typeof history !== "undefined" && typeof window[patchKey] === "undefined") {
  for (const type of [eventPushState, eventReplaceState]) {
    const original = history[type];
    // TODO: we should be using unstable_batchedUpdates to avoid multiple re-renders,
    // however that will require an additional peer dependency on react-dom.
    // See: https://github.com/reactwg/react-18/discussions/86#discussioncomment-1567149
    history[type] = function () {
      const result = original.apply(this, arguments);
      const event = new Event(type);
      event.arguments = arguments;

      dispatchEvent(event);
      return result;
    };
  }

  // patch history object only once
  // See: https://github.com/molefrog/wouter/issues/167
  Object.defineProperty(window, patchKey, { value: true });
}


================================================
FILE: packages/wouter/src/use-hash-location.d.ts
================================================
export * from "../types/use-hash-location.js";


================================================
FILE: packages/wouter/src/use-hash-location.js
================================================
import { useSyncExternalStore } from "./react-deps.js";

// array of callback subscribed to hash updates
const listeners = {
  v: [],
};

const onHashChange = () => listeners.v.forEach((cb) => cb());

// we subscribe to `hashchange` only once when needed to guarantee that
// all listeners are called synchronously
const subscribeToHashUpdates = (callback) => {
  if (listeners.v.push(callback) === 1)
    addEventListener("hashchange", onHashChange);

  return () => {
    listeners.v = listeners.v.filter((i) => i !== callback);
    if (!listeners.v.length) removeEventListener("hashchange", onHashChange);
  };
};

// leading '#' is ignored, leading '/' is optional
const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, "");

export const navigate = (to, { state = null, replace = false } = {}) => {
  const oldURL = location.href;

  const [hash, search] = to.replace(/^#?\/?/, "").split("?");

  // Works for ALL protocols including data:
  const url = new URL(location.href);
  url.hash = `/${hash}`;
  if (search) url.search = search;
  const newURL = url.href;

  if (replace) {
    history.replaceState(state, "", newURL);
  } else {
    history.pushState(state, "", newURL);
  }

  const event =
    typeof HashChangeEvent !== "undefined"
      ? new HashChangeEvent("hashchange", { oldURL, newURL })
      : new Event("hashchange", { detail: { oldURL, newURL } });

  dispatchEvent(event);
};

export const useHashLocation = ({ ssrPath = "/" } = {}) => [
  useSyncExternalStore(
    subscribeToHashUpdates,
    currentHashLocation,
    () => ssrPath
  ),
  navigate,
];

useHashLocation.hrefs = (href) => "#" + href;


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


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


================================================
FILE: packages/wouter/test/history-patch.test.ts
================================================
import { useLocation as reactHook } from "../src/index.js";
import { useLocation as preactHook } from "../src/index.js";
import { renderHook, act } from "@testing-library/react";

import { mock, test, expect, describe } from "bun:test";

describe("history patch", () => {
  test("exports should exists", () => {
    expect(reactHook).toBeDefined();
    expect(preactHook).toBeDefined();
  });

  test("history should be patched once", () => {
    const fn = mock();
    const { result, unmount } = renderHook(() => reactHook());

    addEventListener("pushState", (e) => {
      fn();
    });

    expect(result.current[0]).toBe("/");
    expect(fn).toHaveBeenCalledTimes(0);

    act(() => result.current[1]("/hello"));
    act(() => result.current[1]("/world"));

    expect(result.current[0]).toBe("/world");
    expect(fn).toHaveBeenCalledTimes(2);

    unmount();
  });
});


================================================
FILE: packages/wouter/test/jest-dom.d.ts
================================================
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";

declare module "bun:test" {
  interface Matchers<T = unknown>
    extends TestingLibraryMatchers<typeof expect.stringContaining, T> {}
}


================================================
FILE: packages/wouter/test/link.test-d.tsx
================================================
import { describe, expectTypeOf, test } from "bun:test";
import { Link, LinkProps, type Path } from "../src/index.js";
import * as React from "react";

type NetworkLocationHook = () => [
  Path,
  (path: string, options: { host: string; retries?: number }) => void
];

describe("<Link /> types", () => {
  test("should have required prop href", () => {
    // @ts-expect-error
    <Link>test</Link>;
    <Link href="/">test</Link>;
  });

  test("does not allow `to` and `href` props to be used at the same time", () => {
    // @ts-expect-error
    <Link to="/hello" href="/world">
      Hello
    </Link>;
  });

  test("should inherit props from `HTMLAnchorElement`", () => {
    <Link to="/hello" className="hello">
      Hello
    </Link>;

    <Link to="/hello" style={{}}>
      Hello
    </Link>;

    <Link to="/hello" target="_blank">
      Hello
    </Link>;

    <Link to="/hello" download ping="he-he">
      Hello
    </Link>;
  });

  test("can accept function as `className`", () => {
    <Link
      href="/"
      className={(isActive) => (isActive ? "active" : "non-active")}
    />;

    <Link
      href="/"
      className={(isActive) => (isActive ? "active" : undefined)}
    />;
  });

  test("should support other navigation params", () => {
    <Link href="/" state={{ a: "foo" }}>
      test
    </Link>;

    <Link href="/" replace>
      test
    </Link>;

    // @ts-expect-error
    <Link to="/" replace={{ nope: 1 }}>
      Hello
    </Link>;

    <Link href="/" state={undefined}>
      test
    </Link>;
  });

  test("should work with generic type", () => {
    <Link<NetworkLocationHook> href="/" host="wouter.com">
      test
    </Link>;

    // @ts-expect-error
    <Link<NetworkLocationHook> href="/">test</Link>;

    <Link<NetworkLocationHook> href="/" host="wouter.com" retries={4}>
      test
    </Link>;
  });
});

describe("<Link /> with ref", () => {
  test("should work", () => {
    const ref = React.useRef<HTMLAnchorElement>(null);

    <Link to="/" ref={ref}>
      Hello
    </Link>;
  });

  test("should have error when type is miss matched", () => {
    const ref = React.useRef<HTMLAreaElement>(null);

    // @ts-expect-error
    <Link to="/" ref={ref}>
      Hello
    </Link>;
  });
});

describe("<Link /> with `asChild` prop", () => {
  test("should work", () => {
    <Link to="/" asChild>
      <a>Hello</a>
    </Link>;
  });

  test("does not allow `to` and `href` props to be used at the same time", () => {
    // @ts-expect-error
    <Link to="/hello" href="/world" asChild>
      <a>Hello</a>
    </Link>;
  });

  test("can only have valid element as a child", () => {
    // @ts-expect-error strings are not valid children
    <Link to="/" asChild>
      {true ? "Hello" : "World"}
    </Link>;

    // @ts-expect-error can't use multiple nodes as children
    <Link to="/" asChild>
      <a>Link</a>
      <div>icon</div>
    </Link>;
  });

  test("does not allow other props", () => {
    // @ts-expect-error
    <Link to="/" asChild className="">
      <a>Hello</a>
    </Link>;

    // @ts-expect-error
    <Link to="/" asChild style={{}}>
      <a>Hello</a>
    </Link>;

    // @ts-expect-error
    <Link to="/" asChild unknown={10}>
      <a>Hello</a>
    </Link>;

    // @ts-expect-error
    <Link to="/" asChild ref={null}>
      <a>Hello</a>
    </Link>;
  });

  test("should support other navigation params", () => {
    <Link to="/" asChild replace>
      <a>Hello</a>
    </Link>;

    // @ts-expect-error
    <Link to="/" asChild replace={12}>
      <a>Hello</a>
    </Link>;

    <Link to="/" asChild state={{ hello: "world" }}>
      <a>Hello</a>
    </Link>;
  });

  test("should work with generic type", () => {
    <Link<NetworkLocationHook> asChild to="/" host="wouter.com">
      <div>test</div>
    </Link>;

    // @ts-expect-error
    <Link<NetworkLocationHook> asChild to="/">
      <div>test</div>
    </Link>;

    <Link<NetworkLocationHook> asChild to="/" host="wouter.com" retries={4}>
      <div>test</div>
    </Link>;
  });

  test("accepts `onClick` prop that overwrites child's handler", () => {
    <Link
      to="/"
      asChild
      onClick={(e) => {
        expectTypeOf(e).toEqualTypeOf<React.MouseEvent>();
      }}
    >
      <a>Hello</a>
    </Link>;
  });

  test("should work with `ComponentProps`", () => {
    type LinkComponentProps = React.ComponentProps<typeof Link>;

    // Because Link is a generic component, the props
    // cant't contain navigation options of the default generic
    // parameter `BrowserLocationHook`.
    // So the best we can get are the props such as `href` etc.
    expectTypeOf<LinkComponentProps>().toMatchTypeOf<LinkProps>();
  });
});


================================================
FILE: packages/wouter/test/link.test.tsx
================================================
import { type MouseEventHandler } from "react";
import { test, expect, afterEach, mock, describe } from "bun:test";
import { render, cleanup, fireEvent, act } from "@testing-library/react";

import { Router, Link } from "../src/index.js";
import { memoryLocation } from "../src/memory-location.js";

afterEach(cleanup);

describe("<Link />", () => {
  test("renders a link with proper attributes", () => {
    const { getByText } = render(
      <Link href="/about" className="link--active">
        Click Me
      </Link>
    );

    const element = getByText("Click Me");

    expect(element).toBeInTheDocument();
    expect(element).toHaveAttribute("href", "/about");
    expect(element).toHaveClass("link--active");
  });

  test("passes ref to <a />", () => {
    const refCallback = mock<(element: HTMLAnchorElement) => void>();
    const { getByText } = render(
      <Link href="/" ref={refCallback}>
        Testing
      </Link>
    );

    const element = getByText("Testing");

    expect(element).toBeInTheDocument();
    expect(element).toHaveAttribute("href", "/");

    expect(refCallback).toHaveBeenCalledTimes(1);
    expect(refCallback).toHaveBeenCalledWith(element);
  });

  test("still creates a plain link when nothing is passed", () => {
    const { getByTestId } = render(<Link href="/about" data-testid="link" />);

    const element = getByTestId("link");

    expect(element).toBeInTheDocument();
    expect(element).toHaveAttribute("href", "/about");
    expect(element).toBeEmptyDOMElement();
  });

  test("supports `to` prop as an alias to `href`", () => {
    const { getByText } = render(<Link to="/about">Hello</Link>);
    const element = getByText("Hello");

    expect(element).toBeInTheDocument();
    expect(element).toHaveAttribute("href", "/about");
  });

  test("performs a navigation when the link is clicked", () => {
    const { getByTestId } = render(
      <Link href="/goo-baz" data-testid="link">
        link
      </Link>
    );

    fireEvent.click(getByTestId("link"));

    expect(location.pathname).toBe("/goo-baz");
  });

  test("supports replace navigation", () => {
    const { getByTestId } = render(
      <Link href="/goo-baz" replace data-testid="link">
        link
      </Link>
    );

    const histBefore = history.length;

    fireEvent.click(getByTestId("link"));

    expect(location.pathname).toBe("/goo-baz");
    expect(history.length).toBe(histBefore);
  });

  test("ignores the navigation when clicked with modifiers", () => {
    const { getByTestId } = render(
      <Link href="/users" data-testid="link">
        click
      </Link>
    );
    const clickEvt = new MouseEvent("click", {
      bubbles: true,
      cancelable: true,
      button: 0,
      ctrlKey: true,
    });

    // js-dom doesn't implement browser navigation (e.g. changing location
    // when a link is clicked) so we need just ingore it to avoid warnings
    clickEvt.preventDefault();

    fireEvent(getByTestId("link"), clickEvt);
    expect(location.pathname).not.toBe("/users");
  });

  test("ignores the navigation when event is cancelled", () => {
    const clickHandler: MouseEventHandler = (e) => {
      e.preventDefault();
    };

    const { getByTestId } = render(
      <Link href="/users" data-testid="link" onClick={clickHandler}>
        click
      </Link>
    );

    fireEvent.click(getByTestId("link"));
    expect(location.pathname).not.toBe("/users");
  });

  test("accepts an `onClick` prop, fired before the navigation", () => {
    const clickHandler = mock();

    const { getByTestId } = render(
      <Link href="/" onClick={clickHandler} data-testid="link" />
    );

    fireEvent.click(getByTestId("link"));
    expect(clickHandler).toHaveBeenCalledTimes(1);
  });

  test("renders `href` with basepath", () => {
    const { getByTestId } = render(
      <Router base="/app">
        <Link href="/dashboard" data-testid="link" />
      </Router>
    );

    const link = getByTestId("link");
    expect(link.getAttribute("href")).toBe("/app/dashboard");
  });

  test("renders `href` with absolute links", () => {
    const { getByTestId } = render(
      <Router base="/app">
        <Link href="~/home" data-testid="link" />
      </Router>
    );

    const element = getByTestId("link");
    expect(element).toHaveAttribute("href", "/home");
  });

  test("supports history state", () => {
    const testState = { hello: "world" };
    const { getByTestId } = render(
      <Link href="/goo-baz" state={testState} data-testid="link">
        link
      </Link>
    );

    fireEvent.click(getByTestId("link"));
    expect(location.pathname).toBe("/goo-baz");
    expect(history.state).toStrictEqual(testState);
  });

  test("can be configured to use custom href formatting", () => {
    const formatter = (href: string) => `#${href}`;

    const { getByTestId } = render(
      <>
        <Router hrefs={formatter}>
          <Link href="/" data-testid="root" />
          <Link href="/home" data-testid="home" />
        </Router>

        <Router base="/app" hrefs={formatter}>
          <Link href="~/home" data-testid="absolute" />
        </Router>
      </>
    );

    expect(getByTestId("root")).toHaveAttribute("href", "#/");
    expect(getByTestId("home")).toHaveAttribute("href", "#/home");
    expect(getByTestId("absolute")).toHaveAttribute("href", "#/home");
  });
});

describe("active links", () => {
  test("proxies `className` when it is a string", () => {
    const { getByText } = render(
      <Link href="/" className="link--active warning">
        Click Me
      </Link>
    );

    const element = getByText("Click Me");
    expect(element).toHaveAttribute("class", "link--active warning");
  });

  test("calls the `className` function with active link flag", () => {
    const { navigate, hook } = memoryLocation({ path: "/" });

    const { getByText } = render(
      <Router hook={hook}>
        <Link
          href="/"
          className={(isActive) => {
            return [isActive ? "active" : "", "link"].join(" ");
          }}
        >
          Click Me
        </Link>
      </Router>
    );

    const element = getByText("Click Me");
    expect(element).toBeInTheDocument();
    expect(element).toHaveClass("active");
    expect(element).toHaveClass("link");

    act(() => navigate("/about"));

    expect(element).not.toHaveClass("active");
    expect(element).toHaveClass("link");
  });

  test("correctly highlights active links when using custom href formatting", () => {
    const formatter = (href: string) => `#${href}`;
    const { navigate, hook } = memoryLocation({ path: "/" });

    const { getByText } = render(
      <Router hook={hook} hrefs={formatter}>
        <Link
          href="/"
          className={(isActive) => {
            return [isActive ? "active" : "", "link"].join(" ");
          }}
        >
          Click Me
        </Link>
      </Router>
    );

    const element = getByText("Click Me");
    expect(element).toBeInTheDocument();
    expect(element).toHaveClass("active");
    expect(element).toHaveClass("link");

    act(() => navigate("/about"));

    expect(element).not.toHaveClass("active");
    expect(element).toHaveClass("link");
  });
});

describe("<Link /> with `asChild` prop", () => {
  test("when `asChild` is not specified, wraps the children in an <a />", () => {
    const { getByText } = render(
      <Link href="/about">
        <div className="link--wannabe">Click Me</div>
      </Link>
    );

    const link = getByText("Click Me");

    expect(link.tagName).toBe("DIV");
    expect(link).not.toHaveAttribute("href");
    expect(link).toHaveClass("link--wannabe");
    expect(link).toHaveTextContent("Click Me");

    expect(link.parentElement?.tagName).toBe("A");
    expect(link.parentElement).toHaveAttribute("href", "/about");
  });

  test("when invalid element is provided, wraps the children in an <a />", () => {
    const { getByText } = render(
      /* @ts-expect-error */
      <Link href="/about" asChild>
        Click Me
      </Link>
    );

    const link = getByText("Click Me");

    expect(link.tagName).toBe("A");
    expect(link).toHaveAttribute("href", "/about");
    expect(link).toHaveTextContent("Click Me");
  });

  test("when more than one element is provided, wraps the children in an <a />", async () => {
    const { getByText } = render(
      /* @ts-expect-error */
      <Link href="/about" asChild>
        <span>1</span>
        <span>2</span>
        <span>3</span>
      </Link>
    );

    const span = getByText("1");

    expect(span.parentElement?.tagName).toBe("A");

    expect(span.parentElement).toHaveAttribute("href", "/about");
    expect(span.parentElement).toHaveTextContent("123");
  });

  test("injects href prop when rendered with `asChild`", () => {
    const { getByText } = render(
      <Link href="/about" asChild>
        <div className="link--wannabe">Click Me</div>
      </Link>
    );

    const link = getByText("Click Me");

    expect(link.tagName).toBe("DIV");
    expect(link).toHaveClass("link--wannabe");
    expect(link).toHaveAttribute("href", "/about");
    expect(link).toHaveTextContent("Click Me");
  });

  test("missing href or to won't crash", () => {
    const { getByText } = render(
      /* @ts-expect-error */
      <Link>Click Me</Link>
    );

    const link = getByText("Click Me");

    expect(link.tagName).toBe("A");
    expect(link).toHaveAttribute("href", undefined);
    expect(link).toHaveTextContent("Click Me");
  });
});


================================================
FILE: packages/wouter/test/location-hook.test-d.ts
================================================
import { test, expectTypeOf, describe } from "bun:test";
import { BaseLocationHook, HookNavigationOptions } from "../src/index.js";

describe("`HookNavigationOptions` utility type", () => {
  test("should return empty interface for hooks with no nav options", () => {
    const hook = (): [string, (path: string) => void] => {
      return ["stub", (path: string) => {}];
    };

    type Options = HookNavigationOptions<typeof hook>;

    expectTypeOf<Options>().toEqualTypeOf<{}>();

    const optionsExt: Options | { a: 1 } = { a: 1, b: 2 };
  });

  test("should return object with required navigation params", () => {
    const hook = (): [
      string,
      (path: string, options: { replace: boolean; optional?: number }) => void
    ] => {
      return ["stub", () => {}];
    };

    type Options = HookNavigationOptions<typeof hook>;

    // @ts-expect-error
    expectTypeOf<Options>().toEqualTypeOf<{
      replace: boolean;
      foo: string;
    }>();

    expectTypeOf<Options>().toEqualTypeOf<{
      replace: boolean;
      optional?: number;
    }>();
  });

  test("should not contain never when options are optional", () => {
    const hook = (
      param: string
    ): [string, (path: string, options?: { replace: boolean }) => void] => {
      return ["stub", () => {}];
    };

    type Options = HookNavigationOptions<typeof hook>;

    expectTypeOf<Options>().toEqualTypeOf<{
      replace: boolean;
    }>();
  });

  test("should only support valid hooks", () => {
    // @ts-expect-error
    type A = HookNavigationOptions<string>;
    // @ts-expect-error
    type B = HookNavigationOptions<{}>;
    // @ts-expect-error
    type C = HookNavigationOptions<() => []>;
  });

  test("should return empty object when `BaseLocationHook` is given", () => {
    type Options = HookNavigationOptions<BaseLocationHook>;
    expectTypeOf<Options>().toEqualTypeOf<{}>();
  });
});


================================================
FILE: packages/wouter/test/match-route.test-d.ts
================================================
import { test, expectTypeOf } from "bun:test";
import { matchRoute, useRouter } from "../src/index.js";

const assertType = <T>(_value: T): void => {};
const { parser } = useRouter();

test("should only accept strings", () => {
  // @ts-expect-error
  assertType(matchRoute(parser, Symbol(), ""));
  // @ts-expect-error
  assertType(matchRoute(parser, undefined, ""));
  assertType(matchRoute(parser, "/", ""));
});

test('has a boolean "match" result as a first returned value', () => {
  const [match] = matchRoute(parser, "/", "");
  expectTypeOf(match).toEqualTypeOf<boolean>();
});

test("returns null as parameters when there was no match", () => {
  const [match, params] = matchRoute(parser, "/foo", "");

  if (!match) {
    expectTypeOf(params).toEqualTypeOf<null>();
  }
});

test("accepts the type of parameters as a generic argument", () => {
  const [match, params] = matchRoute<{ id: string; name: string | undefined }>(
    parser,
    "/app/users/:name?/:id",
    ""
  );

  if (match) {
    expectTypeOf(params).toEqualTypeOf<{
      id: string;
      name: string | undefined;
    }>();
  }
});

test("infers parameters from the route path", () => {
  const [, inferedParams] = matchRoute(parser, "/app/users/:name?/:id/*?", "");

  if (inferedParams) {
    expectTypeOf(inferedParams).toMatchTypeOf<{
      0?: string;
      1?: string;
      2?: string;
      name?: string;
      id: string;
      wildcard?: string;
    }>();
  }
});


================================================
FILE: packages/wouter/test/memory-location.test-d.ts
================================================
import { test, expectTypeOf } from "bun:test";
import { memoryLocation } from "../src/memory-location.js";
import { BaseLocationHook } from "../src/index.js";

const assertType = <T>(_value: T): void => {};

test("should return hook that supports location spec", () => {
  const { hook } = memoryLocation();

  expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();

  const [location, navigate] = hook();

  assertType<string>(location);
  assertType<Function>(navigate);
});

test("should return `navigate` method for navigating outside of components", () => {
  const { navigate } = memoryLocation();

  assertType<Function>(navigate);
});

test("should support `record` option for saving the navigation history", () => {
  const { history, reset } = memoryLocation({ record: true });

  assertType<string[]>(history);
  assertType<Function>(reset);
});

test("should have history only wheen record is true", () => {
  // @ts-expect-error
  const { history, reset } = memoryLocation({ record: false });
  assertType(history);
  assertType(reset);
});

test("should support initial path", () => {
  const { hook } = memoryLocation({ path: "/initial-path" });

  expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
});

test("should support `static` option", () => {
  const { hook } = memoryLocation({ static: true });

  expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
});


================================================
FILE: packages/wouter/test/memory-location.test.ts
================================================
import { test, expect } from "bun:test";
import { renderHook, act } from "@testing-library/react";
import { memoryLocation } from "../src/memory-location.js";

test("returns a hook that is compatible with location spec", () => {
  const { hook } = memoryLocation();

  const { result, unmount } = renderHook(() => hook());
  const [value, update] = result.current;

  expect(typeof value).toBe("string");
  expect(typeof update).toBe("function");
  unmount();
});

test("should support initial path", () => {
  const { hook } = memoryLocation({ path: "/test-case" });

  const { result, unmount } = renderHook(() => hook());
  const [value] = result.current;

  expect(value).toBe("/test-case");
  unmount();
});

test("should support initial path with query", () => {
  const { searchHook } = memoryLocation({ path: "/test-case?foo=bar" });

  const { result, unmount } = renderHook(() => searchHook());
  const value = result.current;

  expect(value).toBe("foo=bar");
  unmount();
});

test("should support search path as parameter", () => {
  const { searchHook } = memoryLocation({
    path: "/test-case?foo=bar",
    searchPath: "key=value",
  });

  const { result, unmount } = renderHook(() => searchHook());
  const value = result.current;

  expect(value).toBe("foo=bar&key=value");
  unmount();
});

test('should return location hook that has initial path "/" by default', () => {
  const { hook } = memoryLocation();

  const { result, unmount } = renderHook(() => hook());
  const [value] = result.current;

  expect(value).toBe("/");
  unmount();
});

test('should return search hook that has initial query "" by default', () => {
  const { searchHook } = memoryLocation();

  const { result, unmount } = renderHook(() => searchHook());
  const value = result.current;

  expect(value).toBe("");
  unmount();
});

test("should return standalone `navigate` method", () => {
  const { hook, navigate } = memoryLocation();

  const { result, unmount } = renderHook(() => hook());

  act(() => navigate("/standalone"));

  const [value] = result.current;
  expect(value).toBe("/standalone");
  unmount();
});

test("should return location hook that supports navigation", () => {
  const { hook } = memoryLocation();

  const { result, unmount } = renderHook(() => hook());

  act(() => result.current[1]("/location"));

  const [value] = result.current;
  expect(value).toBe("/location");
  unmount();
});

test("should record all history when `record` option is provided", () => {
  const {
    hook,
    history,
    navigate: standalone,
  } = memoryLocation({ record: true, path: "/test" });

  const { result, unmount } = renderHook(() => hook());

  act(() => standalone("/standalone"));
  act(() => result.current[1]("/location"));

  expect(result.current[0]).toBe("/location");

  expect(history).toStrictEqual(["/test", "/standalone", "/location"]);

  act(() => standalone("/standalone", { replace: true }));

  expect(history).toStrictEqual(["/test", "/standalone", "/standalone"]);

  act(() => result.current[1]("/location", { replace: true }));

  expect(history).toStrictEqual(["/test", "/standalone", "/location"]);

  unmount();
});

test("should not have history when `record` option is falsy", () => {
  // @ts-expect-error
  const { history, reset } = memoryLocation();
  expect(history).not.toBeDefined();
  expect(reset).not.toBeDefined();
});

test("should have reset method when `record` option is provided", () => {
  const { history, reset, navigate } = memoryLocation({
    path: "/initial",
    record: true,
  });
  expect(history).toBeDefined();
  expect(reset).toBeDefined();

  navigate("test-1");
  navigate("test-2");

  reset();

  expect(history).toStrictEqual(["/initial"]);
});

test("should have reset method that reset hook location", () => {
  const { hook, history, navigate, reset } = memoryLocation({
    record: true,
    path: "/test",
  });
  const { result, unmount } = renderHook(() => hook());

  act(() => navigate("/location"));

  expect(result.current[0]).toBe("/location");

  expect(history).toStrictEqual(["/test", "/location"]);

  act(() => reset());

  expect(history).toStrictEqual(["/test"]);

  expect(result.current[0]).toBe("/test");

  unmount();
});


================================================
FILE: packages/wouter/test/nested-route.test.tsx
================================================
import { test, expect, describe } from "bun:test";
import { act, render, renderHook } from "@testing-library/react";

import { Route, Router, Switch, useRouter } from "../src/index.js";
import { memoryLocation } from "../src/memory-location.js";

describe("when `nest` prop is given", () => {
  test("renders by default", () => {
    const { container } = render(<Route nest>matched!</Route>);
    expect(container.innerHTML).toBe("matched!");
  });

  test("matches the pattern loosely", () => {
    const { hook, navigate } = memoryLocation();

    const { container } = render(
      <Router hook={hook}>
        <Route path="/posts/:slug" nest>
          matched!
        </Route>
      </Router>
    );

    expect(container.innerHTML).toBe("");

    act(() => navigate("/posts/all")); // full match
    expect(container.innerHTML).toBe("matched!");

    act(() => navigate("/users"));
    expect(container.innerHTML).toBe("");

    act(() => navigate("/posts/10-react-tricks/table-of-contents"));
    expect(container.innerHTML).toBe("matched!");
  });

  test("can be used inside a Switch", () => {
    const { container } = render(
      <Router
        hook={
          memoryLocation({ path: "/posts/13/2012/sort", static: true }).hook
        }
      >
        <Switch>
          <Route path="/about">about</Route>
          <Route path="/posts/:slug" nest>
            nested
          </Route>
          <Route>default</Route>
        </Switch>
      </Router>
    );

    expect(container.innerHTML).toBe("nested");
  });

  test("sets the base to the matched segment", () => {
    const { result } = renderHook(() => useRouter().base, {
      wrapper: (props) => (
        <Router
          hook={memoryLocation({ path: "/2012/04/posts", static: true }).hook}
        >
          <Route path="/:year/:month" nest>
            <Route path="/posts">{props.children}</Route>
          </Route>
        </Router>
      ),
    });

    expect(result.current).toBe("/2012/04");
  });

  test("can be nested in another nested `Route` or `Router`", () => {
    const { container } = render(
      <Router
        base="/app"
        hook={
          memoryLocation({
            path: "/app/users/alexey/settings/all",
            static: true,
          }).hook
        }
      >
        <Route path="/users/:name" nest>
          <Route path="/settings">should not be rendered</Route>

          <Route path="/settings" nest>
            <Route path="/all">All settings</Route>
          </Route>
        </Route>
      </Router>
    );

    expect(container.innerHTML).toBe("All settings");
  });

  test("reacts to `nest` updates", () => {
    const { hook } = memoryLocation({
      path: "/app/apple/products",
      static: true,
    });

    const App = ({ nested }: { nested: boolean }) => {
      return (
        <Router hook={hook}>
          <Route path="/app/:company" nest={nested}>
            matched!
          </Route>
        </Router>
      );
    };

    const { container, rerender } = render(<App nested={true} />);
    expect(container.innerHTML).toBe("matched!");

    rerender(<App nested={false} />);
    expect(container.innerHTML).toBe("");
  });

  test("works with one optional segment", () => {
    const { hook, navigate } = memoryLocation({
      path: "/",
    });

    const App = () => {
      return (
        <Router hook={hook}>
          <Route path="/:version?" nest>
            {({ version }) => version ?? "default"}
          </Route>
        </Router>
      );
    };

    const { container } = render(<App />);
    expect(container.innerHTML).toBe("default");

    act(() => navigate("/v1"));
    expect(container.innerHTML).toBe("v1");

    act(() => navigate("/v2/dashboard"));
    expect(container.innerHTML).toBe("v2");
  });
});


================================================
FILE: packages/wouter/test/parser.test.tsx
================================================
import { test, expect } from "bun:test";

import { pathToRegexp, Key } from "path-to-regexp";
import { renderHook } from "@testing-library/react";

import { Router, useRouter, useRoute, Parser } from "../src/index.js";
import { memoryLocation } from "../src/memory-location.js";

// Custom parser that uses `path-to-regexp` instead of `regexparam`
const pathToRegexpParser: Parser = (route: string) => {
  const keys: Key[] = [];
  const pattern = pathToRegexp(route, keys);

  return { pattern, keys: keys.map((k) => String(k.name)) };
};

test("overrides the `parser` prop on the current router", () => {
  const { result } = renderHook(() => useRouter(), {
    wrapper: ({ children }) => (
      <Router parser={pathToRegexpParser}>{children}</Router>
    ),
  });

  const router = result.current;
  expect(router.parser).toBe(pathToRegexpParser);
});

test("allows to change the behaviour of route matching", () => {
  const { result } = renderHook(
    () => useRoute("/(home|dashboard)/:pages?/users/:rest*"),
    {
      wrapper: ({ children }) => (
        <Router
          hook={memoryLocation({ path: "/home/users/10/bio" }).hook}
          parser={pathToRegexpParser}
        >
          {children}
        </Router>
      ),
    }
  );

  expect(result.current).toStrictEqual([
    true,
    {
      0: "home",
      1: undefined,
      2: "10/bio",
      pages: undefined,
      rest: "10/bio",
    } as any,
  ]);
});


================================================
FILE: packages/wouter/test/redirect.test-d.tsx
================================================
import { describe, test } from "bun:test";
import { Redirect } from "../src/index.js";

const assertType = <T,>(_value: T): void => {};

describe("Redirect types", () => {
  test("should have required prop href", () => {
    // @ts-expect-error
    assertType(<Redirect />);
    assertType(<Redirect href="/" />);
  });

  test("should support state prop", () => {
    assertType(<Redirect href="/" state={{ a: "foo" }} />);
    assertType(<Redirect href="/" state={null} />);
    assertType(<Redirect href="/" state={undefined} />);
    assertType(<Redirect href="/" state="string" />);
  });

  test("always renders nothing", () => {
    // can be used in JSX
    <div>
      <Redirect href="/" />
    </div>;

    assertType<null>(Redirect({ href: "/" }));
  });

  test("can not accept children", () => {
    // @ts-expect-error
    <Redirect href="/">hi!</Redirect>;

    // prettier-ignore
    // @ts-expect-error
    <Redirect href="/"><><div>Fragment</div></></Redirect>;
  });
});


================================================
FILE: packages/wouter/test/redirect.test.tsx
================================================
import { test, expect } from "bun:test";
import { render } from "@testing-library/react";
import { useState } from "react";

import { Redirect, Router } from "../src/index.js";

export const customHookWithReturn =
  (initialPath = "/") =>
  () => {
    const [path, updatePath] = useState(initialPath);
    const navigate = (path: string) => {
      updatePath(path);
      return "foo";
    };

    return [path, navigate];
  };

test("renders nothing", () => {
  const { container, unmount } = render(<Redirect to="/users" />);
  expect(container.childNodes.length).toBe(0);
  unmount();
});

test("results in change of current location", () => {
  const { unmount } = render(<Redirect to="/users" />);

  expect(location.pathname).toBe("/users");
  unmount();
});

test("supports `base` routers with relative path", () => {
  const { unmount } = render(
    <Router base="/app">
      <Redirect to="/nested" />
    </Router>
  );

  expect(location.pathname).toBe("/app/nested");
  unmount();
});

test("supports `base` routers with absolute path", () => {
  const { unmount } = render(
    <Router base="/app">
      <Redirect to="~/absolute" />
    </Router>
  );

  expect(location.pathname).toBe("/absolute");
  unmount();
});

test("supports replace navigation", () => {
  const histBefore = history.length;

  const { unmount } = render(<Redirect to="/users" replace />);

  expect(location.pathname).toBe("/users");
  expect(history.length).toBe(histBefore);
  unmount();
});

test("supports history state", () => {
  const testState = { hello: "world" };
  const { unmount } = render(<Redirect to="/users" state={testState} />);

  expect(location.pathname).toBe("/users");
  expect(history.state).toStrictEqual(testState);
  unmount();
});

test("useLayoutEffect should return nothing", () => {
  const { unmount } = render(
    // @ts-expect-error
    <Router hook={customHookWithReturn()}>
      <Redirect to="/users" replace />
    </Router>
  );

  expect(location.pathname).toBe("/users");
  unmount();
});


================================================
FILE: packages/wouter/test/route.test-d.tsx
================================================
import { test, describe, expectTypeOf } from "bun:test";
import { Route } from "../src/index.js";
import { ComponentProps } from "react";
import * as React from "react";

const assertType = <T,>(_value: T): void => {};

describe("`path` prop", () => {
  test("is optional", () => {
    assertType(<Route />);
  });

  test("should be a string or RegExp", () => {
    let a: ComponentProps<typeof Route>["path"];
    expectTypeOf(a).toMatchTypeOf<string | RegExp | undefined>();
  });
});

test("accepts the optional boolean `nest` prop", () => {
  assertType(<Route nest />);
  assertType(<Route nest={false} />);

  // @ts-expect-error - should be boolean
  assertType(<Route nest={"true"} />);
});

test("renders a component provided in the `component` prop", () => {
  const Header = () => <div />;
  const Profile = () => null;

  <Route path="/header" component={Header} />;
  <Route path="/profile/:id" component={Profile} />;

  // @ts-expect-error must be a component, not JSX
  <Route path="/header" component={<a />} />;
});

test("accepts class components in the `component` prop", () => {
  class A extends React.Component<{ params: {} }> {
    render() {
      return <div />;
    }
  }

  <Route path="/app" component={A} />;
});

test("accepts ForwardRefExoticComponent in the `component` prop", () => {
  // Simulates components wrapped with HOCs like withErrorBoundary
  const MyComponent = React.forwardRef<HTMLDivElement, { params: {} }>(
    ({ params }) => <div />
  );

  <Route path="/app" component={MyComponent} />;
});

test("accepts children", () => {
  <Route path="/app">
    <div />
  </Route>;

  <Route path="/app">
    This is a <b>mixed</b> content
  </Route>;

  <Route>
    <>
      <div />
    </>
  </Route>;
});

test("supports functions as children", () => {
  <Route path="/users/:id">
    {(params) => {
      expectTypeOf(params).toMatchTypeOf<{}>();
      return <div />;
    }}
  </Route>;

  <Route path="/users/:id">{({ id }) => `User id: ${id}`}</Route>;

  <Route path="/users/:id">
    {({ age }: { age: string }) => `User age: ${age}`}
  </Route>;

  // @ts-expect-error function should return valid JSX
  <Route path="/app">{() => {}}</Route>;

  // prettier-ignore
  // @ts-expect-error you can't use JSX together with render function
  <Route path="/">{() => <div />}<a>Link</a></Route>;
});

describe("parameter inference", () => {
  test("can infer type of params from the path given", () => {
    <Route path="/path/:first/:second/another/:third">
      {({ first, second, third }) => {
        expectTypeOf(first).toEqualTypeOf<string>();
        return <div>{`${first}, ${second}, ${third}`}</div>;
      }}
    </Route>;

    <Route path="/users/:name/">
      {/* @ts-expect-error - `age` param is not present in the pattern */}
      {({ name, age }) => {
        return <div>{`Hello, ${name}`}</div>;
      }}
    </Route>;
  });

  test("extract wildcard params into `wild` property", () => {
    <Route path="/users/*/settings">
      {({ wild }) => {
        expectTypeOf(wild).toEqualTypeOf<string>();
        return <div>The path is {wild}</div>;
      }}
    </Route>;
  });

  test("allows to customize type of params via generic parameter", () => {
    <Route<{ name: string; lastName: string }> path="/users/:name/:age">
      {(params) => {
        expectTypeOf(params.lastName).toEqualTypeOf<string>();
        return <div>This really is undefined {params.lastName}</div>;
      }}
    </Route>;
  });

  test("can't infer the type when the path isn't known at compile time", () => {
    <Route path={JSON.parse('"/home/:section"')}>
      {(params) => {
        // @ts-expect-error
        params.section;
        return <div />;
      }}
    </Route>;
  });
});


================================================
FILE: packages/wouter/test/route.test.tsx
================================================
import { it, expect, afterEach } from "bun:test";
import { render, act, cleanup } from "@testing-library/react";

import { Router, Route } from "../src/index.js";
import { memoryLocation } from "../src/memory-location.js";
import { ReactElement } from "react";

// Clean up after each test to avoid DOM pollution
afterEach(cleanup);

const testRouteRender = (initialPath: string, jsx: ReactElement) => {
  return render(
    <Router hook={memoryLocation({ path: initialPath }).hook}>{jsx}</Router>
  );
};

it("always renders its content when `path` is empty", () => {
  const { container } = testRouteRender(
    "/nothing",
    <Route>
      <h1>Hello!</h1>
    </Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("Hello!");
});

it("accepts plain children", () => {
  const { container } = testRouteRender(
    "/foo",
    <Route path="/foo">
      <h1>Hello!</h1>
    </Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("Hello!");
});

it("works with render props", () => {
  const { container } = testRouteRender(
    "/foo",
    <Route path="/foo">{() => <h1>Hello!</h1>}</Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("Hello!");
});

it("passes a match param object to the render function", () => {
  const { container } = testRouteRender(
    "/users/alex",
    <Route path="/users/:name">{(params) => <h1>{params.name}</h1>}</Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("alex");
});

it("renders nothing when there is not match", () => {
  const { container } = testRouteRender(
    "/bar",
    <Route path="/foo">
      <div>Hi!</div>
    </Route>
  );

  expect(container.querySelector("div")).not.toBeInTheDocument();
});

it("supports `component` prop similar to React-Router", () => {
  const Users = () => <h2>All users</h2>;

  const { container } = testRouteRender(
    "/foo",
    <Route path="/foo" component={Users} />
  );

  const heading = container.querySelector("h2");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("All users");
});

it("supports `base` routers with relative path", () => {
  const { container, unmount } = render(
    <Router base="/app">
      <Route path="/nested">
        <h1>Nested</h1>
      </Route>
      <Route path="~/absolute">
        <h2>Absolute</h2>
      </Route>
    </Router>
  );

  act(() => history.replaceState(null, "", "/app/nested"));

  expect(container.children).toHaveLength(1);
  expect(container.firstChild).toHaveProperty("tagName", "H1");

  unmount();
});

it("supports `path` prop with regex", () => {
  const { container } = testRouteRender(
    "/foo",
    <Route path={/[/]foo/}>
      <h1>Hello!</h1>
    </Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("Hello!");
});

it("supports regex path named params", () => {
  const { container } = testRouteRender(
    "/users/alex",
    <Route path={/[/]users[/](?<name>[a-z]+)/}>
      {(params) => <h1>{params.name}</h1>}
    </Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("alex");
});

it("supports regex path anonymous params", () => {
  const { container } = testRouteRender(
    "/users/alex",
    <Route path={/[/]users[/]([a-z]+)/}>
      {(params) => <h1>{params[0]}</h1>}
    </Route>
  );

  const heading = container.querySelector("h1");
  expect(heading).toBeInTheDocument();
  expect(heading).toHaveTextContent("alex");
});

it("rejects when a path does not match the regex", () => {
  const { container } = testRouteRender(
    "/users/1234",
    <Route path={/[/]users[/](?<name>[a-z]+)/}>
      {(params) => <h1>{params.name}</h1>}
    </Route>
  );

  expect(container.querySelector("h1")).not.toBeInTheDocument();
});


================================================
FILE: packages/wouter/test/router.test-d.tsx
================================================
import { ComponentProps } from "react";
import { test, expectTypeOf } from "bun:test";
import {
  Router,
  Route,
  BaseLocationHook,
  useRouter,
  Parser,
  Path,
} from "../src/index.js";

test("should have at least one child", () => {
  // @ts-expect-error
  <Router />;
});

test("accepts valid elements as children", () => {
  const Header = ({ title }: { title: string }) => <h1>{title}</h1>;

  <Router>
    <Route path="/" />
    <b>Hello!</b>
  </Router>;

  <Router>
    Hello, we have <Header title="foo" /> and some {1337} numbers here.
  </Router>;

  <Router>
    <>Fragments!</>
  </Router>;

  <Router>
    {/* @ts-expect-error should be a valid element */}
    {() => <div />}
  </Router>;
});

test("can be customized with router properties passed as props", () => {
  // @ts-expect-error
  <Router hook="wat?" />;

  const useFakeLocation: BaseLocationHook = () => ["/foo", () => {}];
  <Router hook={useFakeLocation}>this is a valid router</Router>;

  let fn: ComponentProps<typeof Router>["hook"];
  expectTypeOf(fn).exclude<undefined>().toBeFunction();

  <Router base="/app">Hello World!</Router>;

  <Router ssrPath="/foo">SSR</Router>;

  <Router base="/users" ssrPath="/users/all" hook={useFakeLocation}>
    Custom
  </Router>;
});

test("accepts `hrefs` function for transforming href strings", () => {
  const router = useRouter();
  expectTypeOf(router.hrefs).toBeFunction();

  <Router hrefs={(href: string) => href + "1"}>0</Router>;

  <Router
    hrefs={(href, router) => {
      expectTypeOf(router).toEqualTypeOf<typeof router>();
      return href + router.base;
    }}
  >
    routers as a second argument
  </Router>;
});

test("accepts `parser` function for generating regular expressions", () => {
  const parser: Parser = (path: Path, loose?: boolean) => {
    return {
      pattern: new RegExp(`^${path}${loose === true ? "(?=$|[/])" : "[/]$"}`),
      keys: [],
    };
  };

  <Router parser={parser}>this is a valid router</Router>;
});

test("does not accept other props", () => {
  const router = useRouter();

  // @ts-expect-error `parent` prop isn't defined
  <Router parent={router}>Parent router</Router>;
});


================================================
FILE: packages/wouter/test/router.test.tsx
================================================
import { memo, ReactElement, cloneElement, ComponentProps } from "react";
import { renderHook, render } from "@testing-library/react";
import { it, expect, describe } from "bun:test";
import {
  Router,
  DefaultParams,
  useRouter,
  Parser,
  BaseLocationHook,
} from "../src/index.js";

it("creates a router object on demand", () => {
  const { result } = renderHook(() => useRouter());
  expect(result.current).toBeInstanceOf(Object);
});

it("creates a router object only once", () => {
  const { result, rerender } = renderHook(() => useRouter());
  const router = result.current;

  rerender();
  expect(result.current).toBe(router);
});

it("does not create new router when <Router /> rerenders", () => {
  const { result, rerender } = renderHook(() => useRouter(), {
    wrapper: (props) => <Router>{props.children}</Router>,
  });
  const router = result.current;

  rerender();
  expect(result.current).toBe(router);
});

it("alters the current router with `parser` and `hook` options", () => {
  const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] });
  const hook: BaseLocationHook = () => ["/foo", () => {}];

  const { result } = renderHook(() => useRouter(), {
    wrapper: (props) => (
      <Router parser={newParser} hook={hook}>
        {props.children}
      </Router>
    ),
  });
  const router = result.current;

  expect(router).toBeInstanceOf(Object);
  expect(router.parser).toBe(newParser);
  expect(router.hook).toBe(hook);
});

it("accepts `ssrPath` and `ssrSearch` params", () => {
  const { result } = renderHook(() => useRouter(), {
    wrapper: (props) => (
      <Router ssrPath="/users" ssrSearch="a=b&c=d">
        {props.children}
      </Router>
    ),
  });

  expect(result.current.ssrPath).toBe("/users");
  expect(result.current.ssrSearch).toBe("a=b&c=d");
});

it("can extract `ssrSearch` from `ssrPath` after the '?' symbol", () => {
  let ssrPath: string | undefined = "/no-search";
  let ssrSearch: string | undefined = undefined;

  const { result, rerender } = renderHook(() => useRouter(), {
    wrapper: (props) => (
      <Router ssrPath={ssrPath} ssrSearch={ssrSearch}>
        {props.children}
      </Router>
    ),
  });

  expect(result.current.ssrPath).toBe("/no-search");
  expect(result.current.ssrSearch).toBe("");

  ssrPath = "/with-search?a=b&c=d";
  rerender();

  expect(result.current.ssrPath).toBe("/with-search");
  expect(result.current.ssrSearch).toBe("a=b&c=d");

  ssrSearch = "x=y&z=w";
  rerender();
  expect(result.current.ssrSearch).toBe("a=b&c=d");
});

it("keeps the ssrSearch undefined if not in SSR mode", () => {
  const { result } = renderHook(() => useRouter(), {
    wrapper: (props) => <Router>{props.children}</Router>,
  });

  expect(result.current.ssrPath).toBe(undefined);
  expect(result.current.ssrSearch).toBe(undefined);
});

it("shares one router instance between components", () => {
  const routers: any[] = [];

  const RouterGetter = ({ index }: { index: number }) => {
    const router = useRouter();
    routers[index] = router;
    return <div data-testid={`router-${index}`} />;
  };

  render(
    <>
      <RouterGetter index={0} />
      <RouterGetter index={1} />
      <RouterGetter index={2} />
      <RouterGetter index={3} />
    </>
  );

  const uniqRouters = [...new Set<DefaultParams>(routers)];
  expect(uniqRouters.length).toBe(1);
});

describe("`base` prop", () => {
  it("is an empty string by default", () => {
    const { result } = renderHook(() => useRouter());
    expect(result.current.base).toBe("");
  });

  it("can be customized via the `base` prop", () => {
    const { result } = renderHook(() => useRouter(), {
      wrapper: (props) => <Router base="/foo">{props.children}</Router>,
    });
    expect(result.current.base).toBe("/foo");
  });

  it("appends provided path to the parent router's base", () => {
    const { result } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router base="/baz">
          <Router base="/foo">
            <Router base="/bar">{props.children}</Router>
          </Router>
        </Router>
      ),
    });
    expect(result.current.base).toBe("/baz/foo/bar");
  });
});

describe("`hook` prop", () => {
  it("when provided, the router isn't inherited from the parent", () => {
    const customHook: BaseLocationHook = () => ["/foo", () => {}];
    const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] });

    const {
      result: { current: router },
    } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router base="/app" parser={newParser}>
          <Router hook={customHook} base="/bar">
            {props.children}
          </Router>
        </Router>
      ),
    });

    expect(router.hook).toBe(customHook);
    expect(router.parser).not.toBe(newParser);
    expect(router.base).toBe("/bar");
  });
});

describe("`hrefs` prop", () => {
  it("sets the router's `hrefs` property", () => {
    const formatter = () => "noop";

    const {
      result: { current: router },
    } = renderHook(() => useRouter(), {
      wrapper: (props) => <Router hrefs={formatter}>{props.children}</Router>,
    });

    expect(router.hrefs).toBe(formatter);
  });

  it("can infer `hrefs` from the `hook`", () => {
    const hookHrefs = () => "noop";
    const hook = (): [string, (v: string) => void] => {
      return ["/foo", () => {}];
    };

    hook.hrefs = hookHrefs;

    let hrefsRouterOption: ((href: string) => string) | undefined;

    const { rerender, result } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router hook={hook} hrefs={hrefsRouterOption}>
          {props.children}
        </Router>
      ),
    });

    expect(result.current.hrefs).toBe(hookHrefs);

    // `hrefs` passed directly to the router should take precedence
    hrefsRouterOption = (href) => "custom formatter";
    rerender();

    expect(result.current.hrefs).toBe(hrefsRouterOption);
  });
});

describe("`aroundNav` prop", () => {
  it("sets the router's `aroundNav` property", () => {
    const aroundNav = () => {};

    const { result } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router aroundNav={aroundNav}>{props.children}</Router>
      ),
    });

    expect(result.current.aroundNav).toBe(aroundNav);
  });

  it("is inherited from parent router", () => {
    const aroundNav = () => {};

    const { result } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router aroundNav={aroundNav}>
          <Router base="/nested">{props.children}</Router>
        </Router>
      ),
    });

    expect(result.current.aroundNav).toBe(aroundNav);
  });

  it("can be overridden in nested router", () => {
    const parentAroundNav = () => {};
    const childAroundNav = () => {};

    const { result } = renderHook(() => useRouter(), {
      wrapper: (props) => (
        <Router aroundNav={parentAroundNav}>
          <Router aroundNav={childAroundNav}>{props.children}</Router>
        </Router>
      ),
    });

    expect(result.current.aroundNav).toBe(childAroundNav);
  });
});

it("updates the context when settings are changed", () => {
  const state: { renders: number } & Partial<ComponentProps<typeof Router>> = {
    renders: 0,
  };

  const Memoized = memo((props) => {
    const router = useRouter();
    state.renders++;

    state.hook = router.hook;
    state.base = router.base;

    return <></>;
  });

  const { rerender } = render(
    <Router base="/app">
      <Memoized />
    </Router>
  );

  expect(state.renders).toEqual(1);
  expect(state.base).toBe("/app");

  rerender(
    <Router base="/app">
      <Memoized />
    </Router>
  );
  expect(state.renders).toEqual(1); // nothing changed

  // should re-render the hook
  const newHook: BaseLocationHook = () => ["/location", () => {}];
  rerender(
    <Router hook={newHook} base="/app">
      <Memoized />
    </Router>
  );
  expect(state.renders).toEqual(2);
  expect(state.base).toEqual("/app");
  expect(state.hook).toEqual(newHook);

  // should update the context when the base changes as well
  rerender(
    <Router hook={newHook} base="">
      <Memoized />
    </Router>
  );
  expect(state.renders).toEqual(3);
  expect(state.base).toEqual("");
  expect(state.hook).toEqual(newHook);

  // the last check that the router context is stable during re-renders
  rerender(
    <Router hook={newHook} base="">
      <Memoized />
    </Router>
  );
  expect(state.renders).toEqual(3); // nothing changed
});


================================================
FILE: packages/wouter/test/setup.ts
================================================
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { expect } from "bun:test";
import * as matchers from "@testing-library/jest-dom/matchers";

// Register happy-dom globals (document, window, etc.)
GlobalRegistrator.register({
  url: "https://wouter.dev",
  width: 1024,
  height: 768,
});

// Extend Bun's expect with jest-dom matchers
(expect as any).extend(matchers);

/**
 * Runs a function with `location` temporarily removed from globalThis.
 * Simulates pure Node.js SSR environment for testing.
 */
export const withoutLocation = <T>(fn: () => T): T => {
  const original = globalThis.location;
  // @ts-expect-error - intentionally removing location
  delete globalThis.location;
  try {
    return fn();
  } finally {
    globalThis.location = original;
  }
};


================================================
FILE: packages/wouter/test/ssr.test.tsx
================================================
import { test, expect, describe } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import {
  Route,
  Router,
  useRoute,
  Link,
  Redirect,
  useSearch,
  useLocation,
  SsrContext,
} from "../src/index.js";
import { withoutLocation } from "./setup.js";

describe("server-side rendering", () => {
  test("works via `ssrPath` prop", () => {
    const App = () => (
      <Router ssrPath="/users/baz">
        <Route path="/users/baz">foo</Route>
        <Route path="/users/:any*">bar</Route>
        <Route path="/users/:id">{(params) => params.id}</Route>
        <Route path="/about">should not be rendered</Route>
      </Router>
    );

    const rendered = renderToStaticMarkup(<App />);
    expect(rendered).toBe("foobarbaz");
  });

  test("supports hook-based routes", () => {
    const HookRoute = () => {
      const [match, params] = useRoute("/pages/:name");
      return <>{match ? `Welcome to ${params.name}!` : "Not Found!"}</>;
    };

    const App = () => (
      <Router ssrPath="/pages/intro">
        <HookRoute />
      </Router>
    );

    const rendered = renderToStaticMarkup(<App />);
    expect(rendered).toBe("Welcome to intro!");
  });

  test("renders valid and accessible link elements", () => {
    const App = () => (
      <Router ssrPath="/">
        <Link href="/users/1" title="Profile">
          Mark
        </Link>
      </Router>
    );

    const rendered = renderToStaticMarkup(<App />);
    expect(rendered).toBe(`<a title="Profile" href="/users/1">Mark</a>`);
  });

  test("renders redirects however they have effect only on a client-side", () => {
    const App = () => (
      <Router ssrPath="/">
        <Route path="/">
          <Redirect to="/foo" />
        </Route>

        <Route path="/foo">You won't see that in SSR page</Route>
      </Router>
    );

    const rendered = renderToStaticMarkup(<App />);
    expect(rendered).toBe("");
  });

  test("update ssr context", () => {
    const context: SsrContext = {};
    const App = () => (
      <Router ssrPath="/" ssrContext={context}>
        <Route path="/">
          <Redirect to="/foo" />
        </Route>
      </Router>
    );

    renderToStaticMarkup(<App />);
    expect(context.redirectTo).toBe("/foo");

    // Clean up - reset context to prevent state leakage
    delete context.redirectTo;
  });

  describe("rendering with given search string", () => {
    test("allows to override search string", () => {
      const App = () => {
        const search = useSearch();
        const [location] = useLocation();

        return (
          <>
            {location} filter by {search}
          </>
        );
      };

      const rendered = renderToStaticMarkup(
        <Router ssrPath="/catalog" ssrSearch="sort=created_at">
          <App />
        </Router>
      );

      expect(rendered).toBe("/catalog filter by sort=created_at");
    });

    test("doesn't break useSearch hook if not specified", () => {
      const PrintSearch = () => <>{useSearch()}</>;

      const rendered = withoutLocation(() =>
        renderToStaticMarkup(
          <Router ssrPath="/">
            <PrintSearch />
          </Router>
        )
      );

      expect(rendered).toBe("");
    });

    test("works with empty ssrSearch", () => {
      const PrintSearch = () => <>{useSearch()}</>;

      const rendered = withoutLocation(() =>
        renderToStaticMarkup(
          <Router ssrPath="/" ssrSearch="">
            <PrintSearch />
          </Router>
        )
      );

      expect(rendered).toBe("");
    });
  });
});


================================================
FILE: packages/wouter/test/switch.test.tsx
================================================
import { it, expect, afterEach } from "bun:test";

import { Router, Route, Switch } from "../src/index.js";
import { memoryLocation } from "../src/memory-location.js";

import { render, act, cleanup } from "@testing-library/react";
import { PropsWithChildren, ReactElement, JSX } from "react";

// Clean up after each test to avoid DOM pollution
afterEach(cleanup);

const raf = () => new Promise((resolve) => requestAnimationFrame(resolve));

const testRouteRender = (initialPath: string, jsx: ReactElement) => {
  return render(
    <Router hook={memoryLocation({ path: initialPath }).hook}>{jsx}</Router>
  );
};

it("works well when nothing is provided", () => {
  const { container } = testRouteRender("/users/12", <Switch>{null}</Switch>);
  // When Switch has no matching children, it renders null, so container should be empty
  expect(container).toBeEmptyDOMElement();
});

it("always renders no more than 1 matched children", () => {
  const { container } = testRouteRender(
    "/users/12",
    <Switch>
      <Route path="/users/home">
        <h1 />
      </Route>
      <Route path="/users/:id">
        <h2 />
      </Route>
      <Route path="/users/:rest*">
        <h3 />
      </Route>
    </Switch>
  );

  // Should only render the h2 that matches /users/:id
  expect(container.querySelectorAll("h1, h2, h3")).toHaveLength(1);
  expect(container.querySelector("h2")).toBeInTheDocument();
  expect(container.querySelector("h1")).not.toBeInTheDocument();
  expect(container.querySelector("h3")).not.toBeInTheDocument();
});

it("ignores mixed children", () => {
  const { container } = testRouteRender(
    "/users",
    <Switch>
      Here is a<Route path="/users">route</Route>
      route
    </Switch>
  );

  // Should only render the route content, ignoring text nodes
  expect(container).toHaveTextContent("route");
  // The text "Here is a" and "route" outside the Route should be ignored
  expect(container.textContent).toBe("route");
});

it("ignores falsy children", () => {
  const { container } = testRouteRender(
    "/users",
    <Switch>
      {""}
      {false}
      {null}
      {undefined}
      <Route path="/users">route</Route>
    </Switch>
  );

  // Should only render the route content
  expect(container).toHaveTextContent("route");
  expect(container.textContent).toBe("route");
});

it("matches regular components as well", () => {
  const Dummy = (props: PropsWithChildren<{ path: string }>) => (
    <>{props.children}</>
  );

  const { container } = testRouteRender(
    "/",
    <Switch>
      <Dummy path="/">Component</Dummy>
      <b>Bold</b>
    </Switch>
  );

  // Should render the Dummy component content
  expect(container).toHaveTextContent("Component");
  expect(container.querySelector("b")).not.toBeInTheDocument();
});

it("allows to specify which routes to render via `location` prop", () => {
  const { container } = testRouteRender(
    "/something-different",
    <Switch location="/users">
      <Route path="/users">route</Route>
    </Switch>
  );

  // Should render based on the location prop, not the actual path
  expect(container).toHaveTextContent("route");
});

it("always ensures the consistency of inner routes rendering", async () => {
  history.replaceState(null, "", "/foo/bar");

  const { unmount } = render(
    <Switch>
      <Route path="/foo/:id">
        {(params) => {
          if (!params)
            throw new Error("Render prop is called with falsy params!");
          return null;
        }}
      </Route>
    </Switch>
  );

  await act(async () => {
    await raf();
    history.pushState(null, "", "/");
  });

  unmount();
});

it("supports catch-all routes with wildcard segments", async () => {
  const { container } = testRouteRender(
    "/something-different",
    <Switch>
      <Route path="/users">
        <h1 />
      </Route>
      <Route path="/:anything*">
        <h2 />
      </Route>
    </Switch>
  );

  // Should match the catch-all route
  expect(container.querySelectorAll("h1, h2")).toHaveLength(1);
  expect(container.querySelector("h2")).toBeInTheDocument();
  expect(container.querySelector("h1")).not.toBeInTheDocument();
});

it("uses a route without a path prop as a fallback", async () => {
  const { container } = testRouteRender(
    "/something-different",
    <Switch>
      <Route path="/users">
        <h1 />
      </Route>
      <Route>
        <h2 />
      </Route>
    </Switch>
  );

  // Should match the fallback route (no path)
  expect(container.querySelectorAll("h1, h2")).toHaveLength(1);
  expect(container.querySelector("h2")).toBeInTheDocument();
  expect(container.querySelector("h1")).not.toBeInTheDocument();
});

it("correctly handles arrays as children", async () => {
  const { container } = testRouteRender(
    "/in-array-3",
    <Switch>
      {[1, 2, 3].map((i) => {
        const H = `h${i}` as keyof JSX.IntrinsicElements;
        return (
          <Route key={i} path={"/in-array-" + i}>
            <H />
          </Route>
        );
      })}
      <Route>
        <h4 />
      </Route>
    </Switch>
  );

  // Should match the third route (/in-array-3)
  expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1);
  expect(container.querySelector("h3")).toBeInTheDocument();
  expect(container.querySelector("h1")).not.toBeInTheDocument();
  expect(container.querySelector("h2")).not.toBeInTheDocument();
  expect(container.querySelector("h4")).not.toBeInTheDocument();
});

it("correctly handles fragments as children", async () => {
  const { container } = testRouteRender(
    "/in-fragment-2",
    <Switch>
      <>
        {[1, 2, 3].map((i) => {
          const H = `h${i}` as keyof JSX.IntrinsicElements;
          return (
            <Route key={i} path={"/in-fragment-" + i}>
              <H />
            </Route>
          );
        })}
      </>
      <Route>
        <h4 />
      </Route>
    </Switch>
  );

  // Should match the second route (/in-fragment-2)
  expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1);
  expect(container.querySelector("h2")).toBeInTheDocument();
  expect(container.querySelector("h1")).not.toBeInTheDocument();
  expect(container.querySelector("h3")).not.toBeInTheDocument();
  expect(container.querySelector("h4")).not.toBeInTheDocument();
});


================================================
FILE: packages/wouter/test/test-utils.ts
================================================
/**
 * Executes a callback and returns a promise that resolve when `hashchange` event is fired.
 * Rejects after `throwAfter` milliseconds.
 */
export const waitForHashChangeEvent = async (
  cb: () => void,
  throwAfter = 1000
) =>
  new Promise<void>((resolve, reject) => {
    let timeout: ReturnType<typeof setTimeout>;

    const onChange = () => {
      resolve();
      clearTimeout(timeout);
      window.removeEventListener("hashchange", onChange);
    };

    window.addEventListener("hashchange", onChange);
    cb();

    timeout = setTimeout(() => {
      reject(new Error("Timed out: `hashchange` event did not fire!"));
      window.removeEventListener("hashchange", onChange);
    }, throwAfter);
  });


================================================
FILE: packages/wouter/test/use-browser-location.test-d.ts
================================================
import { test, describe, expectTypeOf } from "bun:test";
import {
  useBrowserLocation,
  useSearch,
  useHistoryState,
} from "../src/use-browser-location.js";

const assertType = <T>(_value: T): void => {};

describe("useBrowserLocation", () => {
  test("should return string, function tuple", () => {
    const [loc, navigate] = useBrowserLocation();

    assertType<string>(loc);
    assertType<Function>(navigate);
  });

  test("should return `navigate` function with `path` and `options` parameters", () => {
    const [, navigate] = useBrowserLocation();

    assertType(navigate("/path"));
    assertType(navigate(""));

    // @ts-expect-error
    assertType(navigate());
    // @ts-expect-error
    assertType(navigate(null));

    assertType(navigate("/path", { replace: true }));
    // @ts-expect-error
    assertType(navigate("/path", { unknownOption: true }));
  });

  test("should support `ssrPath` option", () => {
    assertType(useBrowserLocation({ ssrPath: "/something" }));
    // @ts-expect-error
    assertType(useBrowserLocation({ foo: "bar" }));
  });
});

describe("useSearch", () => {
  test("should return string", () => {
    type Search = ReturnType<typeof useSearch>;
    const search = useSearch();

    assertType<string>(search);
    const allowedSearchValues: Search[] = ["", "?leading", "no-?-sign"];
  });
});

describe("useHistoryState", () => {
  test("should support generics", () => {
    type TestCase = { hello: string };
    const state = useHistoryState<TestCase>();

    expectTypeOf(state).toEqualTypeOf<TestCase>();
  });

  test("should fallback to any when type doesn't provided", () => {
    const state = useHistoryState();

    expectTypeOf(state).toEqualTypeOf<any>();
  });
});


================================================
FILE: packages/wouter/test/use-browser-location.test.tsx
================================================
import { useEffect } from "react";
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { renderHook, act, waitFor, cleanup } from "@testing-library/react";
import {
  useBrowserLocation,
  navigate,
  useSearch,
  useHistoryState,
} from "../src/use-browser-location.js";

afterEach(cleanup);

test("returns a pair [value, update]", () => {
  const { result, unmount } = renderHook(() => useBrowserLocation());
  const [value, update] = result.current;

  expect(typeof value).toBe("string");
  expect(typeof update).toBe("function");
  unmount();
});

describe("`value` first argument", () => {
  beforeEach(() => history.replaceState(null, "", "/"));

  test("reflects the current pathname", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    expect(result.current[0]).toBe("/");
    unmount();
  });

  test("reacts to `pushState` / `replaceState`", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());

    act(() => history.pushState(null, "", "/foo"));
    expect(result.current[0]).toBe("/foo");

    act(() => history.replaceState(null, "", "/bar"));
    expect(result.current[0]).toBe("/bar");
    unmount();
  });

  test("supports history.back() navigation", async () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());

    act(() => history.pushState(null, "", "/foo"));
    await waitFor(() => expect(result.current[0]).toBe("/foo"));

    act(() => {
      history.back();
    });

    // Workaround for happy-dom: manually dispatch popstate event
    // happy-dom doesn't fully implement history.back() popstate events
    act(() => {
      const popstateEvent = new PopStateEvent("popstate", {
        state: history.state,
      });
      window.dispatchEvent(popstateEvent);
    });

    await waitFor(() => expect(result.current[0]).toBe("/"), { timeout: 1000 });
    unmount();
  });

  test("supports history state", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    const { result: state, unmount: unmountState } = renderHook(() =>
      useHistoryState()
    );

    const navigate = result.current[1];

    act(() => navigate("/path", { state: { hello: "world" } }));

    expect(state.current).toStrictEqual({ hello: "world" });

    unmount();
    unmountState();
  });

  test("uses fail-safe escaping", () => {
    const { result } = renderHook(() => useBrowserLocation());
    const navigate = result.current[1];

    act(() => navigate("/%not-valid"));
    expect(result.current[0]).toBe("/%not-valid");

    act(() => navigate("/99%"));
    expect(result.current[0]).toBe("/99%");
  });
});

describe("`useSearch` hook", () => {
  beforeEach(() => history.replaceState(null, "", "/"));

  test("allows to get current search string", () => {
    const { result: searchResult } = renderHook(() => useSearch());
    act(() => navigate("/foo?hello=world&whats=up"));

    expect(searchResult.current).toBe("?hello=world&whats=up");
  });

  test("returns empty string when there is no search string", () => {
    const { result: searchResult } = renderHook(() => useSearch());

    expect(searchResult.current).toBe("");

    act(() => navigate("/foo"));
    expect(searchResult.current).toBe("");

    act(() => navigate("/foo? "));
    expect(searchResult.current).toBe("");
  });

  test("does not re-render when only pathname is changed", () => {
    // count how many times each hook is rendered
    const locationRenders = { current: 0 };
    const searchRenders = { current: 0 };

    // count number of rerenders for each hook
    renderHook(() => {
      useEffect(() => {
        locationRenders.current += 1;
      });
      return useBrowserLocation();
    });

    renderHook(() => {
      useEffect(() => {
        searchRenders.current += 1;
      });
      return useSearch();
    });

    expect(locationRenders.current).toBe(1);
    expect(searchRenders.current).toBe(1);

    act(() => navigate("/foo"));

    expect(locationRenders.current).toBe(2);
    expect(searchRenders.current).toBe(1);

    act(() => navigate("/foo?bar"));
    expect(locationRenders.current).toBe(2); // no re-render
    expect(searchRenders.current).toBe(2);

    act(() => navigate("/baz?bar"));
    expect(locationRenders.current).toBe(3); // no re-render
    expect(searchRenders.current).toBe(2);
  });
});

describe("`update` second parameter", () => {
  test("rerenders the component", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    const update = result.current[1];

    act(() => update("/about"));
    expect(result.current[0]).toBe("/about");
    unmount();
  });

  test("changes the current location", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    const update = result.current[1];

    act(() => update("/about"));
    expect(location.pathname).toBe("/about");
    unmount();
  });

  test("saves a new entry in the History object", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    const update = result.current[1];

    const histBefore = history.length;
    act(() => update("/about"));

    expect(history.length).toBe(histBefore + 1);
    unmount();
  });

  test("replaces last entry with a new entry in the History object", () => {
    const { result, unmount } = renderHook(() => useBrowserLocation());
    const update = result.current[1];

    const histBefore = history.length;
    act(() => update("/foo", { replace: true }));

    expect(history.length).toBe(histBefore);
    expect(location.pathname).toBe("/foo");
    unmount();
  });

  test("stays the same reference between re-renders (function ref)", () => {
    const { result, rerender, unmount } = renderHook(() =>
      useBrowserLocation()
    );

    const updateWas = result.current[1];
    rerender();
    const updateNow = result.current[1];

    expect(updateWas).toBe(updateNow);
    unmount();
  });
});


================================================
FILE: packages/wouter/test/use-hash-location.test-d.ts
================================================
import { test, describe, expectTypeOf } from "bun:test";
import { useHashLocation, navigate } from "../src/use-hash-location.js";
import { BaseLocationHook } from "../src/index.js";

const assertType = <T>(_value: T): void => {};

test("is a location hook", () => {
  expectTypeOf(useHashLocation).toMatchTypeOf<BaseLocationHook>();
  expectTypeOf(useHashLocation()).toMatchTypeOf<[string, Function]>();
});

test("accepts a `ssrPath` path option", () => {
  useHashLocation({ ssrPath: "/foo" });
  useHashLocation({ ssrPath: "" });

  // @ts-expect-error
  useHashLocation({ base: 123 });
  // @ts-expect-error
  useHashLocation({ unknown: "/base" });
});

describe("`navigate` function", () => {
  test("accepts an arbitrary `state` option", () => {
    navigate("/object", { state: { foo: "bar" } });
    navigate("/symbol", { state: Symbol("foo") });
    navigate("/string", { state: "foo" });
    navigate("/undef", { state: undefined });
  });

  test("returns nothing", () => {
    assertType<void>(navigate("/foo"));
  });
});


================================================
FILE: packages/wouter/test/use-hash-location.test.tsx
================================================
import { test, expect, beforeEach, mock } from "bun:test";
import { renderHook, render, act } from "@testing-library/react";
import { renderToStaticMarkup } from "react-dom/server";

import { Router, Route, useLocation, Link } from "../src/index.js";
import { useHashLocation } from "../src/use-hash-location.js";

import { waitForHashChangeEvent } from "./test-utils";
import { ReactNode, useSyncExternalStore } from "react";

beforeEach(() => {
  history.replaceState(null, "", "/");
  location.hash = "";
});

test("gets current location from `location.hash`", () => {
  location.hash = "/app/users";
  const { result } = renderHook(() => useHashLocation());
  const [path] = result.current;

  expect(path).toBe("/app/users");
});

test("isn't sensitive to leading slash", () => {
  location.hash = "app/users";
  const { result } = renderHook(() => useHashLocation());
  const [path] = result.current;

  expect(path).toBe("/app/users");
});

test("rerenders when hash changes", async () => {
  const { result } = renderHook(() => useHashLocation());

  expect(result.current[0]).toBe("/");

  await act(async () => {
    await waitForHashChangeEvent(() => {
      location.hash = "/app/users";
    });
  });

  expect(result.current[0]).toBe("/app/users");
});

test("changes current hash when navigation is performed", () => {
  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  act(() => {
    navigate("/app/users");
  });
  expect(location.hash).toBe("#/app/users");
});

test("should not rerender when pathname changes", () => {
  let renderCount = 0;
  location.hash = "/app";

  const { result } = renderHook(() => {
    useHashLocation();
    return ++renderCount;
  });

  expect(result.current).toBe(1);
  history.replaceState(null, "", "/foo?bar#/app");

  expect(result.current).toBe(1);
});

test("does not change anything besides the hash when doesn't contain ? symbol", () => {
  history.replaceState(null, "", "/foo?bar#/app");

  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  act(() => {
    navigate("/settings/general");
  });
  expect(location.pathname).toBe("/foo");
  expect(location.search).toBe("?bar");
});

test("changes search and hash when contains ? symbol", () => {
  history.replaceState(null, "", "/foo?bar#/app");

  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  act(() => {
    navigate("/abc?def");
  });
  expect(location.pathname).toBe("/foo");
  expect(location.search).toBe("?def");
  expect(location.hash).toBe("#/abc");
});

test("creates a new history entry when navigating", () => {
  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  const initialLength = history.length;
  act(() => {
    navigate("/about");
  });
  expect(history.length).toBe(initialLength + 1);
});

test("supports `state` option when navigating", () => {
  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  act(() => {
    navigate("/app/users", { state: { hello: "world" } });
  });
  expect(history.state).toStrictEqual({ hello: "world" });
});

test("never changes reference to `navigate` between rerenders", () => {
  const { result, rerender } = renderHook(() => useHashLocation());

  const updateWas = result.current[1];
  rerender();

  expect(result.current[1]).toBe(updateWas);
});

test("uses `ssrPath` when rendered on the server", () => {
  const App = () => {
    const [path] = useHashLocation({ ssrPath: "/hello-from-server" });
    return <>{path}</>;
  };

  const rendered = renderToStaticMarkup(<App />);
  expect(rendered).toBe("/hello-from-server");
});

test("is not sensitive to leading / or # when navigating", async () => {
  const { result } = renderHook(() => useHashLocation());
  const [, navigate] = result.current;

  await act(async () => {
    await waitForHashChangeEvent(() => navigate("look-ma-no-slashes"));
  });
  expect(location.hash).toBe("#/look-ma-no-slashes");
  expect(result.current[0]).toBe("/look-ma-no-slashes");

  await act(async () =>
Download .txt
gitextract_4azr5gv8/

├── .cursor/
│   └── rules/
│       ├── contributing.mdc
│       └── publishing.mdc
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci-tests.yml
│       └── size.yml
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── README.md
├── bunfig.toml
├── package.json
├── packages/
│   ├── magazin/
│   │   ├── .gitignore
│   │   ├── App.tsx
│   │   ├── client.tsx
│   │   ├── components/
│   │   │   ├── footer.tsx
│   │   │   ├── navbar.tsx
│   │   │   ├── star-wouter.tsx
│   │   │   └── with-status-code.tsx
│   │   ├── db/
│   │   │   └── products.ts
│   │   ├── index.html
│   │   ├── index.tsx
│   │   ├── package.json
│   │   ├── routes/
│   │   │   ├── 404.tsx
│   │   │   ├── about.tsx
│   │   │   ├── cart.tsx
│   │   │   ├── home.tsx
│   │   │   └── products/
│   │   │       └── [slug].tsx
│   │   ├── styles.css
│   │   └── tsconfig.json
│   ├── wouter/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.d.ts
│   │   │   ├── index.js
│   │   │   ├── memory-location.d.ts
│   │   │   ├── memory-location.js
│   │   │   ├── paths.js
│   │   │   ├── react-deps.js
│   │   │   ├── use-browser-location.d.ts
│   │   │   ├── use-browser-location.js
│   │   │   ├── use-hash-location.d.ts
│   │   │   ├── use-hash-location.js
│   │   │   ├── use-sync-external-store.js
│   │   │   └── use-sync-external-store.native.js
│   │   ├── test/
│   │   │   ├── history-patch.test.ts
│   │   │   ├── jest-dom.d.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
│   │   │   ├── setup.ts
│   │   │   ├── 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
│   │   │   └── view-transitions.test.tsx
│   │   └── 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
│   └── wouter-preact/
│       ├── .gitignore
│       ├── package.json
│       ├── src/
│       │   └── react-deps.js
│       ├── test/
│       │   └── preact.test.tsx
│       └── 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
├── specs/
│   └── view-transitions-spec.md
└── tsconfig.json
Download .txt
SYMBOL INDEX (118 symbols across 32 files)

FILE: packages/magazin/App.tsx
  function App (line 12) | function App() {

FILE: packages/magazin/components/footer.tsx
  function Footer (line 3) | function Footer() {

FILE: packages/magazin/components/navbar.tsx
  function Logo (line 3) | function Logo() {
  function NavLink (line 7) | function NavLink({
  function Navbar (line 29) | function Navbar() {

FILE: packages/magazin/components/star-wouter.tsx
  function StarWouter (line 3) | function StarWouter() {

FILE: packages/magazin/components/with-status-code.tsx
  function WithStatusCode (line 3) | function WithStatusCode({

FILE: packages/magazin/db/products.ts
  type Product (line 1) | interface Product {
  function getProductBySlug (line 104) | function getProductBySlug(slug: string): Product | undefined {

FILE: packages/magazin/index.tsx
  method fetch (line 56) | async fetch(req) {

FILE: packages/magazin/routes/404.tsx
  function NotFoundPage (line 4) | function NotFoundPage() {

FILE: packages/magazin/routes/about.tsx
  function Feature (line 4) | function Feature({ children }: { children: React.ReactNode }) {
  function AboutPage (line 13) | function AboutPage() {

FILE: packages/magazin/routes/cart.tsx
  function NotificationBanner (line 13) | function NotificationBanner({
  function CartPage (line 36) | function CartPage() {

FILE: packages/magazin/routes/home.tsx
  function ProductCard (line 6) | function ProductCard({ slug, brand, category, name, price, image }: Prod...
  function CategoryFilter (line 47) | function CategoryFilter({
  function SortSelect (line 73) | function SortSelect({
  function HomePage (line 98) | function HomePage() {

FILE: packages/magazin/routes/products/[slug].tsx
  function ProductPage (line 5) | function ProductPage({ slug }: { slug: string }) {

FILE: packages/wouter-preact/src/react-deps.js
  function is (line 27) | function is(x, y) {
  function useSyncExternalStore (line 30) | function useSyncExternalStore(subscribe, getSnapshot, getSSRSnapshot) {
  function forwardRef (line 63) | function forwardRef(component) {

FILE: packages/wouter-preact/test/preact.test.tsx
  function loadPreact (line 35) | async function loadPreact(): Promise<typeof WouterPreact> {

FILE: packages/wouter-preact/types/index.d.ts
  type StringRouteParams (line 33) | type StringRouteParams<T extends string> = RouteParams<T> & {
  type RegexRouteParams (line 36) | type RegexRouteParams = { [key: string | number]: string | undefined };
  type DefaultParams (line 41) | interface DefaultParams {
  type Params (line 45) | type Params<T extends DefaultParams = DefaultParams> = T;
  type MatchWithParams (line 47) | type MatchWithParams<T extends DefaultParams = DefaultParams> = [
  type NoMatch (line 51) | type NoMatch = [false, null];
  type Match (line 52) | type Match<T extends DefaultParams = DefaultParams> =
  type RouteComponentProps (line 60) | interface RouteComponentProps<T extends DefaultParams = DefaultParams> {
  type RouteProps (line 64) | interface RouteProps<
  type NavigationalProps (line 99) | type NavigationalProps<
  type AsChildProps (line 104) | type AsChildProps<ComponentProps, DefaultElementProps> =
  type HTMLLinkAttributes (line 108) | type HTMLLinkAttributes = Omit<JSX.HTMLAttributes, "className"> & {
  type LinkProps (line 112) | type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
  type RedirectProps (line 119) | type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
  type SwitchProps (line 138) | interface SwitchProps {
  type RouterProps (line 148) | type RouterProps = RouterOptions & {
  type URLSearchParamsInit (line 181) | type URLSearchParamsInit = ConstructorParameters<
  type SetSearchParams (line 185) | type SetSearchParams = (

FILE: packages/wouter-preact/types/location-hook.d.ts
  type Path (line 5) | type Path = string;
  type PathPattern (line 7) | type PathPattern = string | RegExp;
  type SearchString (line 9) | type SearchString = string;
  type HrefsFormatter (line 11) | type HrefsFormatter = (href: string, router?: any) => string;
  type BaseLocationHook (line 15) | type BaseLocationHook = {
  type BaseSearchHook (line 21) | type BaseSearchHook = (...args: any[]) => SearchString;
  type HookReturnValue (line 28) | type HookReturnValue<H extends BaseLocationHook> = ReturnType<H>;
  type HookNavigationOptions (line 31) | type HookNavigationOptions<H extends BaseLocationHook> =

FILE: packages/wouter-preact/types/memory-location.d.ts
  type Navigate (line 3) | type Navigate<S = any> = (
  type HookReturnValue (line 8) | type HookReturnValue = { hook: BaseLocationHook; navigate: Navigate };
  type StubHistory (line 9) | type StubHistory = { history: Path[]; reset: () => void };

FILE: packages/wouter-preact/types/router.d.ts
  type Parser (line 9) | type Parser = (
  type NavigateOptions (line 15) | type NavigateOptions<S = any> = {
  type AroundNavHandler (line 23) | type AroundNavHandler = (
  type RouterObject (line 30) | interface RouterObject {
  type SsrContext (line 44) | type SsrContext = {
  type RouterOptions (line 52) | type RouterOptions = {

FILE: packages/wouter-preact/types/use-browser-location.d.ts
  type Primitive (line 3) | type Primitive = string | number | bigint | boolean | null | undefined |...
  type BrowserSearchHook (line 9) | type BrowserSearchHook = (options?: {
  type BrowserLocationHook (line 31) | type BrowserLocationHook = (options?: {

FILE: packages/wouter/src/index.js
  function useSearchParams (line 221) | function useSearchParams() {

FILE: packages/wouter/src/memory-location.js
  function reset (line 56) | function reset() {

FILE: packages/wouter/test/jest-dom.d.ts
  type Matchers (line 4) | interface Matchers<T = unknown>

FILE: packages/wouter/test/link.test-d.tsx
  type NetworkLocationHook (line 5) | type NetworkLocationHook = () => [
  type LinkComponentProps (line 198) | type LinkComponentProps = React.ComponentProps<typeof Link>;

FILE: packages/wouter/test/location-hook.test-d.ts
  type Options (line 10) | type Options = HookNavigationOptions<typeof hook>;
  type Options (line 25) | type Options = HookNavigationOptions<typeof hook>;
  type Options (line 46) | type Options = HookNavigationOptions<typeof hook>;
  type A (line 55) | type A = HookNavigationOptions<string>;
  type B (line 57) | type B = HookNavigationOptions<{}>;
  type C (line 59) | type C = HookNavigationOptions<() => []>;
  type Options (line 63) | type Options = HookNavigationOptions<BaseLocationHook>;

FILE: packages/wouter/test/route.test-d.tsx
  class A (line 39) | class A extends React.Component<{ params: {} }> {
    method render (line 40) | render() {

FILE: packages/wouter/test/use-browser-location.test-d.ts
  type Search (line 43) | type Search = ReturnType<typeof useSearch>;
  type TestCase (line 53) | type TestCase = { hello: string };

FILE: packages/wouter/test/use-location.test.tsx
  function createContainer (line 18) | function createContainer(

FILE: packages/wouter/types/index.d.ts
  type StringRouteParams (line 38) | type StringRouteParams<T extends string> = RouteParams<T> & {
  type RegexRouteParams (line 41) | type RegexRouteParams = { [key: string | number]: string | undefined };
  type DefaultParams (line 46) | interface DefaultParams {
  type Params (line 50) | type Params<T extends DefaultParams = DefaultParams> = T;
  type MatchWithParams (line 52) | type MatchWithParams<T extends DefaultParams = DefaultParams> = [
  type NoMatch (line 56) | type NoMatch = [false, null];
  type Match (line 57) | type Match<T extends DefaultParams = DefaultParams> =
  type RouteComponentProps (line 65) | interface RouteComponentProps<T extends DefaultParams = DefaultParams> {
  type RouteProps (line 69) | interface RouteProps<
  type NavigationalProps (line 104) | type NavigationalProps<
  type RedirectProps (line 109) | type RedirectProps<H extends BaseLocationHook = BrowserLocationHook> =
  type AsChildProps (line 119) | type AsChildProps<ComponentProps, DefaultElementProps> =
  type HTMLLinkAttributes (line 123) | type HTMLLinkAttributes = Omit<
  type LinkProps (line 130) | type LinkProps<H extends BaseLocationHook = BrowserLocationHook> =
  type SwitchProps (line 146) | interface SwitchProps {
  type RouterProps (line 156) | type RouterProps = RouterOptions & {
  type URLSearchParamsInit (line 189) | type URLSearchParamsInit = ConstructorParameters<
  type SetSearchParams (line 192) | type SetSearchParams = (

FILE: packages/wouter/types/location-hook.d.ts
  type Path (line 5) | type Path = string;
  type PathPattern (line 7) | type PathPattern = string | RegExp;
  type SearchString (line 9) | type SearchString = string;
  type HrefsFormatter (line 11) | type HrefsFormatter = (href: string, router?: any) => string;
  type BaseLocationHook (line 15) | type BaseLocationHook = {
  type BaseSearchHook (line 21) | type BaseSearchHook = (...args: any[]) => SearchString;
  type HookReturnValue (line 28) | type HookReturnValue<H extends BaseLocationHook> = ReturnType<H>;
  type EmptyInterfaceWhenAnyOrNever (line 31) | type EmptyInterfaceWhenAnyOrNever<T> = 0 extends 1 & T
  type HookNavigationOptions (line 38) | type HookNavigationOptions<H extends BaseLocationHook> =

FILE: packages/wouter/types/memory-location.d.ts
  type Navigate (line 8) | type Navigate<S = any> = (
  type HookReturnValue (line 13) | type HookReturnValue = {
  type StubHistory (line 18) | type StubHistory = { history: Path[]; reset: () => void };

FILE: packages/wouter/types/router.d.ts
  type Parser (line 9) | type Parser = (
  type NavigateOptions (line 15) | type NavigateOptions<S = any> = {
  type AroundNavHandler (line 23) | type AroundNavHandler = (
  type RouterObject (line 30) | interface RouterObject {
  type SsrContext (line 44) | type SsrContext = {
  type RouterOptions (line 52) | type RouterOptions = {

FILE: packages/wouter/types/use-browser-location.d.ts
  type Primitive (line 3) | type Primitive = string | number | bigint | boolean | null | undefined |...
  type BrowserSearchHook (line 9) | type BrowserSearchHook = (options?: {
  type BrowserLocationHook (line 31) | type BrowserLocationHook = (options?: {
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (278K chars).
[
  {
    "path": ".cursor/rules/contributing.mdc",
    "chars": 6273,
    "preview": "---\ndescription:\nglobs:\nalwaysApply: true\n---\n\n# Wouter - Minimalist React Router\n\n## Project Overview\n\nWouter is a **mi"
  },
  {
    "path": ".cursor/rules/publishing.mdc",
    "chars": 3046,
    "preview": "---\ndescription: \nglobs: \nalwaysApply: true\n---\n# Publishing Wouter Packages\n\nThis document outlines the process for pub"
  },
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 68,
    "preview": "# These are supported funding model platforms\n\ngithub: [\"molefrog\"]\n"
  },
  {
    "path": ".github/workflows/ci-tests.yml",
    "chars": 893,
    "preview": "name: Run Tests & Linters\n\non:\n  push:\n    branches: \"*\"\n  pull_request:\n    branches: \"*\"\n\njobs:\n  build:\n    runs-on: "
  },
  {
    "path": ".github/workflows/size.yml",
    "chars": 752,
    "preview": "name: Size\non: [pull_request]\njobs:\n  size:\n    runs-on: ubuntu-latest\n    env:\n      CI_JOB_NUMBER: 1\n    steps:\n      "
  },
  {
    "path": ".gitignore",
    "chars": 391,
    "preview": "# NPM\nnpm-debug.log*\nnode_modules/\n.npm\n\n# IDEs\n.vscode/\n*.code-workspace\n\n# bundler\nesm/\n.cache\n\n# OSX\n.DS_Store\n.Apple"
  },
  {
    "path": "CLAUDE.md",
    "chars": 2606,
    "preview": "---\ndescription: Use Bun instead of Node.js, npm, pnpm, or vite.\nglobs: \"*.ts, *.tsx, *.html, *.css, *.js, *.jsx, packag"
  },
  {
    "path": "LICENSE",
    "chars": 1211,
    "preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
  },
  {
    "path": "README.md",
    "chars": 40924,
    "preview": "<div align=\"center\">\n  <img src=\"assets/logo.svg\" width=\"80\" alt=\"Wouter — a super-tiny React router (logo by Katya Sima"
  },
  {
    "path": "bunfig.toml",
    "chars": 126,
    "preview": "[test]\npreload = [\"./packages/wouter/test/setup.ts\"]\ncoverageSkipTestFiles = true\ncoveragePathIgnorePatterns = [\"**/test"
  },
  {
    "path": "package.json",
    "chars": 3542,
    "preview": "{\n  \"name\": \"monorepo\",\n  \"private\": true,\n  \"description\": \"A minimalistic routing for React and Preact. Monorepo packa"
  },
  {
    "path": "packages/magazin/.gitignore",
    "chars": 388,
    "preview": "# dependencies (bun install)\nnode_modules\n\n# output\nout\ndist\n*.tgz\n\n# code coverage\ncoverage\n*.lcov\n\n# logs\nlogs\n_.log\nr"
  },
  {
    "path": "packages/magazin/App.tsx",
    "chars": 1529,
    "preview": "import { Route, Switch, Redirect } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/react-helmet\";\nimport { HomePage }"
  },
  {
    "path": "packages/magazin/client.tsx",
    "chars": 873,
    "preview": "import { hydrateRoot } from \"react-dom/client\";\nimport { flushSync } from \"react-dom\";\nimport { Router, type AroundNavHa"
  },
  {
    "path": "packages/magazin/components/footer.tsx",
    "chars": 1261,
    "preview": "import { Link } from \"wouter\";\n\nexport function Footer() {\n  return (\n    <div className=\"max-w-4xl w-full px-6 self-cen"
  },
  {
    "path": "packages/magazin/components/navbar.tsx",
    "chars": 1472,
    "preview": "import { Link } from \"wouter\";\n\nfunction Logo() {\n  return <i className=\"iconoir-spark-solid text-2xl text-indigo-500\" /"
  },
  {
    "path": "packages/magazin/components/star-wouter.tsx",
    "chars": 1154,
    "preview": "import { useState, useEffect } from \"react\";\n\nexport function StarWouter() {\n  const [stars, setStars] = useState<number"
  },
  {
    "path": "packages/magazin/components/with-status-code.tsx",
    "chars": 326,
    "preview": "import { useRouter } from \"wouter\";\n\nexport function WithStatusCode({\n  code,\n  children,\n}: {\n  code: number;\n  childre"
  },
  {
    "path": "packages/magazin/db/products.ts",
    "chars": 3014,
    "preview": "export interface Product {\n  slug: string;\n  name: string;\n  price: number;\n  brand: string;\n  category: string;\n  image"
  },
  {
    "path": "packages/magazin/index.html",
    "chars": 529,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "packages/magazin/index.tsx",
    "chars": 4588,
    "preview": "import { renderToReadableStream } from \"react-dom/server\";\nimport { Router } from \"wouter\";\nimport {\n  HelmetProvider,\n "
  },
  {
    "path": "packages/magazin/package.json",
    "chars": 490,
    "preview": "{\n  \"name\": \"magazin\",\n  \"module\": \"index.ts\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"bun --w"
  },
  {
    "path": "packages/magazin/routes/404.tsx",
    "chars": 841,
    "preview": "import { Link } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/react-helmet\";\n\nexport function NotFoundPage() {\n  re"
  },
  {
    "path": "packages/magazin/routes/about.tsx",
    "chars": 2363,
    "preview": "import { Link } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/react-helmet\";\n\nfunction Feature({ children }: { chil"
  },
  {
    "path": "packages/magazin/routes/cart.tsx",
    "chars": 3370,
    "preview": "import { useEffect, useState } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/r"
  },
  {
    "path": "packages/magazin/routes/home.tsx",
    "chars": 6046,
    "preview": "import { useSearchParams, Link } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/react-helmet\";\nimport { products, ty"
  },
  {
    "path": "packages/magazin/routes/products/[slug].tsx",
    "chars": 2267,
    "preview": "import { Link } from \"wouter\";\nimport { Helmet } from \"@dr.pogodin/react-helmet\";\nimport { getProductBySlug } from \"@/db"
  },
  {
    "path": "packages/magazin/styles.css",
    "chars": 252,
    "preview": "@import \"tailwindcss\";\n\n/* View Transitions */\n@view-transition {\n  navigation: auto;\n}\n\n/* Default: simple 0.25s cross-"
  },
  {
    "path": "packages/magazin/tsconfig.json",
    "chars": 804,
    "preview": "{\n  \"compilerOptions\": {\n    // Environment setup & latest features\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\""
  },
  {
    "path": "packages/wouter/package.json",
    "chars": 1633,
    "preview": "{\n  \"name\": \"wouter\",\n  \"version\": \"3.9.0\",\n  \"description\": \"Minimalist-friendly ~1.5KB router for React\",\n  \"type\": \"m"
  },
  {
    "path": "packages/wouter/src/index.d.ts",
    "chars": 35,
    "preview": "export * from \"../types/index.js\";\n"
  },
  {
    "path": "packages/wouter/src/index.js",
    "chars": 11807,
    "preview": "import { parse as parsePattern } from \"regexparam\";\n\nimport {\n  useBrowserLocation,\n  useSearch as useBrowserSearch,\n} f"
  },
  {
    "path": "packages/wouter/src/memory-location.d.ts",
    "chars": 45,
    "preview": "export * from \"../types/memory-location.js\";\n"
  },
  {
    "path": "packages/wouter/src/memory-location.js",
    "chars": 1755,
    "preview": "import mitt from \"mitt\";\nimport { useSyncExternalStore } from \"./react-deps.js\";\n\n/**\n * In-memory location that support"
  },
  {
    "path": "packages/wouter/src/paths.js",
    "chars": 1038,
    "preview": "/*\n * Transforms `path` into its relative `base` version\n * If base isn't part of the path provided returns absolute pat"
  },
  {
    "path": "packages/wouter/src/react-deps.js",
    "chars": 2462,
    "preview": "import * as React from \"react\";\n\n// React.useInsertionEffect is not available in React <18\n// This hack fixes a transpil"
  },
  {
    "path": "packages/wouter/src/use-browser-location.d.ts",
    "chars": 50,
    "preview": "export * from \"../types/use-browser-location.js\";\n"
  },
  {
    "path": "packages/wouter/src/use-browser-location.js",
    "chars": 3133,
    "preview": "import { useSyncExternalStore } from \"./react-deps.js\";\n\n/**\n * History API docs @see https://developer.mozilla.org/en-U"
  },
  {
    "path": "packages/wouter/src/use-hash-location.d.ts",
    "chars": 47,
    "preview": "export * from \"../types/use-hash-location.js\";\n"
  },
  {
    "path": "packages/wouter/src/use-hash-location.js",
    "chars": 1646,
    "preview": "import { useSyncExternalStore } from \"./react-deps.js\";\n\n// array of callback subscribed to hash updates\nconst listeners"
  },
  {
    "path": "packages/wouter/src/use-sync-external-store.js",
    "chars": 78,
    "preview": "export { useSyncExternalStore } from \"use-sync-external-store/shim/index.js\";\n"
  },
  {
    "path": "packages/wouter/src/use-sync-external-store.native.js",
    "chars": 85,
    "preview": "export { useSyncExternalStore } from \"use-sync-external-store/shim/index.native.js\";\n"
  },
  {
    "path": "packages/wouter/test/history-patch.test.ts",
    "chars": 879,
    "preview": "import { useLocation as reactHook } from \"../src/index.js\";\nimport { useLocation as preactHook } from \"../src/index.js\";"
  },
  {
    "path": "packages/wouter/test/jest-dom.d.ts",
    "chars": 220,
    "preview": "import type { TestingLibraryMatchers } from \"@testing-library/jest-dom/matchers\";\n\ndeclare module \"bun:test\" {\n  interfa"
  },
  {
    "path": "packages/wouter/test/link.test-d.tsx",
    "chars": 4698,
    "preview": "import { describe, expectTypeOf, test } from \"bun:test\";\nimport { Link, LinkProps, type Path } from \"../src/index.js\";\ni"
  },
  {
    "path": "packages/wouter/test/link.test.tsx",
    "chars": 9527,
    "preview": "import { type MouseEventHandler } from \"react\";\nimport { test, expect, afterEach, mock, describe } from \"bun:test\";\nimpo"
  },
  {
    "path": "packages/wouter/test/location-hook.test-d.ts",
    "chars": 1902,
    "preview": "import { test, expectTypeOf, describe } from \"bun:test\";\nimport { BaseLocationHook, HookNavigationOptions } from \"../src"
  },
  {
    "path": "packages/wouter/test/match-route.test-d.ts",
    "chars": 1457,
    "preview": "import { test, expectTypeOf } from \"bun:test\";\nimport { matchRoute, useRouter } from \"../src/index.js\";\n\nconst assertTyp"
  },
  {
    "path": "packages/wouter/test/memory-location.test-d.ts",
    "chars": 1383,
    "preview": "import { test, expectTypeOf } from \"bun:test\";\nimport { memoryLocation } from \"../src/memory-location.js\";\nimport { Base"
  },
  {
    "path": "packages/wouter/test/memory-location.test.ts",
    "chars": 4228,
    "preview": "import { test, expect } from \"bun:test\";\nimport { renderHook, act } from \"@testing-library/react\";\nimport { memoryLocati"
  },
  {
    "path": "packages/wouter/test/nested-route.test.tsx",
    "chars": 3790,
    "preview": "import { test, expect, describe } from \"bun:test\";\nimport { act, render, renderHook } from \"@testing-library/react\";\n\nim"
  },
  {
    "path": "packages/wouter/test/parser.test.tsx",
    "chars": 1434,
    "preview": "import { test, expect } from \"bun:test\";\n\nimport { pathToRegexp, Key } from \"path-to-regexp\";\nimport { renderHook } from"
  },
  {
    "path": "packages/wouter/test/redirect.test-d.tsx",
    "chars": 990,
    "preview": "import { describe, test } from \"bun:test\";\nimport { Redirect } from \"../src/index.js\";\n\nconst assertType = <T,>(_value: "
  },
  {
    "path": "packages/wouter/test/redirect.test.tsx",
    "chars": 2024,
    "preview": "import { test, expect } from \"bun:test\";\nimport { render } from \"@testing-library/react\";\nimport { useState } from \"reac"
  },
  {
    "path": "packages/wouter/test/route.test-d.tsx",
    "chars": 3738,
    "preview": "import { test, describe, expectTypeOf } from \"bun:test\";\nimport { Route } from \"../src/index.js\";\nimport { ComponentProp"
  },
  {
    "path": "packages/wouter/test/route.test.tsx",
    "chars": 4131,
    "preview": "import { it, expect, afterEach } from \"bun:test\";\nimport { render, act, cleanup } from \"@testing-library/react\";\n\nimport"
  },
  {
    "path": "packages/wouter/test/router.test-d.tsx",
    "chars": 2166,
    "preview": "import { ComponentProps } from \"react\";\nimport { test, expectTypeOf } from \"bun:test\";\nimport {\n  Router,\n  Route,\n  Bas"
  },
  {
    "path": "packages/wouter/test/router.test.tsx",
    "chars": 8503,
    "preview": "import { memo, ReactElement, cloneElement, ComponentProps } from \"react\";\nimport { renderHook, render } from \"@testing-l"
  },
  {
    "path": "packages/wouter/test/setup.ts",
    "chars": 795,
    "preview": "import { GlobalRegistrator } from \"@happy-dom/global-registrator\";\nimport { expect } from \"bun:test\";\nimport * as matche"
  },
  {
    "path": "packages/wouter/test/ssr.test.tsx",
    "chars": 3578,
    "preview": "import { test, expect, describe } from \"bun:test\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport {\n  R"
  },
  {
    "path": "packages/wouter/test/switch.test.tsx",
    "chars": 6296,
    "preview": "import { it, expect, afterEach } from \"bun:test\";\n\nimport { Router, Route, Switch } from \"../src/index.js\";\nimport { mem"
  },
  {
    "path": "packages/wouter/test/test-utils.ts",
    "chars": 719,
    "preview": "/**\n * Executes a callback and returns a promise that resolve when `hashchange` event is fired.\n * Rejects after `throwA"
  },
  {
    "path": "packages/wouter/test/use-browser-location.test-d.ts",
    "chars": 1735,
    "preview": "import { test, describe, expectTypeOf } from \"bun:test\";\nimport {\n  useBrowserLocation,\n  useSearch,\n  useHistoryState,\n"
  },
  {
    "path": "packages/wouter/test/use-browser-location.test.tsx",
    "chars": 5979,
    "preview": "import { useEffect } from \"react\";\nimport { test, expect, describe, beforeEach, afterEach } from \"bun:test\";\nimport { re"
  },
  {
    "path": "packages/wouter/test/use-hash-location.test-d.ts",
    "chars": 1035,
    "preview": "import { test, describe, expectTypeOf } from \"bun:test\";\nimport { useHashLocation, navigate } from \"../src/use-hash-loca"
  },
  {
    "path": "packages/wouter/test/use-hash-location.test.tsx",
    "chars": 9714,
    "preview": "import { test, expect, beforeEach, mock } from \"bun:test\";\nimport { renderHook, render, act } from \"@testing-library/rea"
  },
  {
    "path": "packages/wouter/test/use-location.test.tsx",
    "chars": 6140,
    "preview": "import { ComponentProps, ReactNode } from \"react\";\nimport { it, expect, describe, beforeEach } from \"bun:test\";\nimport {"
  },
  {
    "path": "packages/wouter/test/use-params.test-d.ts",
    "chars": 990,
    "preview": "import { test, expectTypeOf } from \"bun:test\";\nimport { useParams } from \"../src/index.js\";\n\ntest(\"does not accept any a"
  },
  {
    "path": "packages/wouter/test/use-params.test.tsx",
    "chars": 5957,
    "preview": "import { act, renderHook, cleanup } from \"@testing-library/react\";\nimport { test, expect, beforeEach, afterEach } from \""
  },
  {
    "path": "packages/wouter/test/use-route.test-d.ts",
    "chars": 1297,
    "preview": "import { test, expectTypeOf } from \"bun:test\";\nimport { useRoute } from \"../src/index.js\";\n\nconst assertType = <T>(_valu"
  },
  {
    "path": "packages/wouter/test/use-route.test.tsx",
    "chars": 7000,
    "preview": "import { renderHook, act } from \"@testing-library/react\";\nimport { useRoute, Match, Router, RegexRouteParams } from \"../"
  },
  {
    "path": "packages/wouter/test/use-search-params.test.tsx",
    "chars": 2051,
    "preview": "import { renderHook, act } from \"@testing-library/react\";\nimport { useSearchParams, Router } from \"../src/index.js\";\nimp"
  },
  {
    "path": "packages/wouter/test/use-search.test.tsx",
    "chars": 2882,
    "preview": "import { renderHook, act, cleanup } from \"@testing-library/react\";\nimport { useSearch, Router } from \"../src/index.js\";\n"
  },
  {
    "path": "packages/wouter/test/view-transitions.test.tsx",
    "chars": 3169,
    "preview": "import { test, expect, describe, mock, afterEach } from \"bun:test\";\nimport { render, cleanup, fireEvent } from \"@testing"
  },
  {
    "path": "packages/wouter/types/index.d.ts",
    "chars": 5300,
    "preview": "// Minimum TypeScript Version: 4.1\n\n// tslint:disable:no-unnecessary-generics\n\nimport {\n  AnchorHTMLAttributes,\n  Functi"
  },
  {
    "path": "packages/wouter/types/location-hook.d.ts",
    "chars": 1201,
    "preview": "/*\n * Foundation: useLocation and paths\n */\n\nexport type Path = string;\n\nexport type PathPattern = string | RegExp;\n\nexp"
  },
  {
    "path": "packages/wouter/types/memory-location.d.ts",
    "chars": 689,
    "preview": "import {\n  BaseLocationHook,\n  BaseSearchHook,\n  Path,\n  SearchString,\n} from \"./location-hook.js\";\n\ntype Navigate<S = a"
  },
  {
    "path": "packages/wouter/types/router.d.ts",
    "chars": 1603,
    "preview": "import {\n  Path,\n  SearchString,\n  BaseLocationHook,\n  BaseSearchHook,\n  HrefsFormatter,\n} from \"./location-hook.js\";\n\ne"
  },
  {
    "path": "packages/wouter/types/use-browser-location.d.ts",
    "chars": 997,
    "preview": "import { Path, SearchString } from \"./location-hook.js\";\n\ntype Primitive = string | number | bigint | boolean | null | u"
  },
  {
    "path": "packages/wouter/types/use-hash-location.d.ts",
    "chars": 259,
    "preview": "import { Path } from \"./location-hook.js\";\n\nexport function navigate<S = any>(\n  to: Path,\n  options?: { state?: S; repl"
  },
  {
    "path": "packages/wouter-preact/.gitignore",
    "chars": 254,
    "preview": "# Copied from wouter during prepublish (react-deps.js stays as the Preact version)\nsrc/index.js\nsrc/memory-location.js\ns"
  },
  {
    "path": "packages/wouter-preact/package.json",
    "chars": 1907,
    "preview": "{\n  \"name\": \"wouter-preact\",\n  \"version\": \"3.9.0\",\n  \"description\": \"Minimalist-friendly ~1.5KB router for Preact\",\n  \"t"
  },
  {
    "path": "packages/wouter-preact/src/react-deps.js",
    "chars": 2288,
    "preview": "import { useState, useLayoutEffect, useEffect, useRef } from \"preact/hooks\";\nexport {\n  isValidElement,\n  createContext,"
  },
  {
    "path": "packages/wouter-preact/test/preact.test.tsx",
    "chars": 4705,
    "preview": "/** @jsx h */\n/** @jsxFrag Fragment */\n/** @jsxImportSource preact */\n\nimport {\n  test,\n  expect,\n  describe,\n  beforeEa"
  },
  {
    "path": "packages/wouter-preact/types/index.d.ts",
    "chars": 5193,
    "preview": "// Minimum TypeScript Version: 4.1\n// tslint:disable:no-unnecessary-generics\n\nimport {\n  JSX,\n  FunctionComponent,\n  Com"
  },
  {
    "path": "packages/wouter-preact/types/location-hook.d.ts",
    "chars": 1048,
    "preview": "/*\n * Foundation: useLocation and paths\n */\n\nexport type Path = string;\n\nexport type PathPattern = string | RegExp;\n\nexp"
  },
  {
    "path": "packages/wouter-preact/types/memory-location.d.ts",
    "chars": 557,
    "preview": "import { BaseLocationHook, Path } from \"./location-hook.js\";\n\ntype Navigate<S = any> = (\n  to: Path,\n  options?: { repla"
  },
  {
    "path": "packages/wouter-preact/types/router.d.ts",
    "chars": 1603,
    "preview": "import {\n  Path,\n  SearchString,\n  BaseLocationHook,\n  BaseSearchHook,\n  HrefsFormatter,\n} from \"./location-hook.js\";\n\ne"
  },
  {
    "path": "packages/wouter-preact/types/use-browser-location.d.ts",
    "chars": 997,
    "preview": "import { Path, SearchString } from \"./location-hook.js\";\n\ntype Primitive = string | number | bigint | boolean | null | u"
  },
  {
    "path": "packages/wouter-preact/types/use-hash-location.d.ts",
    "chars": 259,
    "preview": "import { Path } from \"./location-hook.js\";\n\nexport function navigate<S = any>(\n  to: Path,\n  options?: { state?: S; repl"
  },
  {
    "path": "specs/view-transitions-spec.md",
    "chars": 3301,
    "preview": "# View Transitions API in Wouter\n\nView Transitions are baseline available (as of Oct 2025). This doc describes the API f"
  },
  {
    "path": "tsconfig.json",
    "chars": 388,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"moduleResolution\": \"bundler\",\n    \"m"
  }
]

About this extraction

This page contains the full source code of the molefrog/wouter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (252.1 KB), approximately 68.1k tokens, and a symbol index with 118 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!