[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[{.*rc,*.yml}]\nindent_style = space\nindent_size = 2\n\n[package.json]\ninsert_final_newline = false\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[test/fixtures/**/*.expected.*]\ntrim_trailing_whitespace = false\ninsert_final_newline = false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n- [ ] Check if updating to the latest `preact-iso` version resolves the issue\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\n\nPlease provide a link to a StackBlitz/CodeSandbox/Codepen project or a GitHub repository that demonstrates the issue. You can use the following template on StackBlitz to get started: https://stackblitz.com/edit/create-preact-starter-routing\n\nIssues without reproductions will likely be closed, they're essential for ensuring we're all on the same page.\n\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. See error\n\n**Expected behavior**\nWhat should have happened when following the steps above?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature request\nassignees: ''\n---\n\n**Describe the feature you'd love to see**\nA clear and concise description of what you'd love to see added to `preact-iso`.\n\n**Additional context (optional)**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - '**'\n\njobs:\n  build_test:\n    name: Test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Tests\n        run: npm run test\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/*\n!.yarn/releases\n!.yarn/plugins\n.pnp.*\n\n# testing\n/coverage\n\n# production\n/dist\n/build\n/.yalc\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@preactjs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 The Preact Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# preact-iso\n\n[![Preact Slack Community](https://img.shields.io/badge/slack-Preact%20Slack%20Community-blue?logo=slack)](https://chat.preactjs.com/)\n\nIsomorphic async tools for Preact.\n\n  - Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration.\n  - Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies.\n  - Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions.\n\n---\n\n  - [Routing](#routing)\n  - [Prerendering](#prerendering)\n  - [Nested Routing](#nested-routing)\n  - [Non-JS Servers](#non-js-servers)\n  - [API Docs](#api-docs)\n    - [\\<LocationProvider\\>](#locationprovider)\n    - [\\<Router\\>](#router)\n    - [\\<Route\\>](#route)\n      - [Path Segment Matching](#path-segment-matching)\n    - [useLocation()](#uselocation)\n    - [useRoute()](#useroute)\n    - [lazy()](#lazy)\n    - [\\<ErrorBoundary\\>](#errorboundary)\n    - [hydrate()](#hydrate)\n    - [prerender()](#prerender)\n    - [locationStub()](#locationstub)\n  - [Navigation API Entry Docs](#navigation-api-entry-docs)\n    - [Differences in usage](#differences-in-usage)\n\n---\n\n## Routing\n\n`preact-iso` offers a simple router for Preact with conventional and hooks-based APIs. The `<Router>` component is async-aware: when transitioning from one route to another, if the incoming route suspends (throws a Promise), the outgoing route is preserved until the new one becomes ready.\n\n```js\nimport { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';\n\n// Synchronous\nimport Home from './routes/home.js';\n\n// Asynchronous (throws a promise)\nconst Profiles = lazy(() => import('./routes/profiles.js'));\nconst Profile = lazy(() => import('./routes/profile.js'));\nconst NotFound = lazy(() => import('./routes/_404.js'));\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<ErrorBoundary>\n\t\t\t<Router>\n\t\t\t\t<Home path=\"/\" />\n\t\t\t\t{/* Alternative dedicated route component for better TS support */}\n\t\t\t\t<Route path=\"/profiles\" component={Profiles} />\n\t\t\t\t<Route path=\"/profile/:id\" component={Profile} />\n\t\t\t\t{/* `default` prop indicates a fallback route. Useful for 404 pages */}\n\t\t\t\t<NotFound default />\n\t\t\t</Router>\n\t\t</ErrorBoundary>\n\t</LocationProvider>\n);\n```\n\n**Progressive Hydration:** When the app is hydrated on the client, the route (`Home` or `Profile` in this case) suspends. This causes hydration for that part of the page to be deferred until the route's `import()` is resolved, at which point that part of the page automatically finishes hydrating.\n\n**Seamless Routing:** When switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route, the router preserves the current route in-place until the incoming route has finished loading, then they are swapped.\n\n## Prerendering\n\n`prerender()` renders a Virtual DOM tree to an HTML string using [`preact-render-to-string`](https://github.com/preactjs/preact-render-to-string). The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page.\n\nPrimarily meant for use with prerendering via [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) or other prerendering systems that share the API. If you're server-side rendering your app via any other method, you can use `preact-render-to-string` (specifically `renderToStringAsync()`) directly.\n\n```js\nimport { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso';\n\n// Asynchronous (throws a promise)\nconst Foo = lazy(() => import('./foo.js'));\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<ErrorBoundary>\n\t\t\t<Router>\n\t\t\t\t<Foo path=\"/\" />\n\t\t\t</Router>\n\t\t</ErrorBoundary>\n\t</LocationProvider>\n);\n\nhydrate(<App />);\n\nexport async function prerender(data) {\n\treturn await ssr(<App />);\n}\n```\n\n## Nested Routing\n\nSome applications would benefit from having routers of multiple levels, allowing to break down the routing logic into smaller components. This is especially useful for larger applications, and we solve this by allowing for multiple nested `<Router>` components.\n\nPartially matched routes end with a wildcard (`/*`) and only the remaining value will be passed to descendant routers for further matching. This allows you to create a parent route that matches a base path, and then have child routes that match specific sub-paths.\n\n```js\nimport { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';\n\nimport AllMovies from './routes/movies/all.js';\n\nconst NotFound = lazy(() => import('./routes/_404.js'));\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<ErrorBoundary>\n\t\t\t<Router>\n\t\t\t\t<Router path=\"/movies\" component={AllMovies} />\n\t\t\t\t<Route path=\"/movies/*\" component={Movies} />\n\t\t\t\t<NotFound default />\n\t\t\t</Router>\n\t\t</ErrorBoundary>\n\t</LocationProvider>\n);\n\nconst TrendingMovies = lazy(() => import('./routes/movies/trending.js'));\nconst SearchMovies = lazy(() => import('./routes/movies/search.js'));\nconst MovieDetails = lazy(() => import('./routes/movies/details.js'));\n\nconst Movies = () => (\n\t<ErrorBoundary>\n\t\t<Router>\n\t\t\t<Route path=\"/trending\" component={TrendingMovies} />\n\t\t\t<Route path=\"/search\" component={SearchMovies} />\n\t\t\t<Route path=\"/:id\" component={MovieDetails} />\n\t\t</Router>\n\t</ErrorBoundary>\n);\n```\n\nThe `<Movies>` component will be used for the following routes:\n  - `/movies/trending`\n  - `/movies/search`\n  - `/movies/Inception`\n  - `/movies/...`\n\nIt will not be used for any of the following:\n  - `/movies`\n  - `/movies/`\n\n## Non-JS Servers\n\nFor those using non-JS servers (e.g., PHP, Python, Ruby, etc.) to serve your Preact app, you may want to use our [\"polyglot-utils\"](./polyglot-utils), a collection of our route matching logic ported to various other languages. Combined with a route manifest, this will allow your server to better understand which assets will be needed at runtime for a given URL, allowing you to say insert preload tags for those assets in the HTML head prior to serving the page.\n\n---\n\n## API Docs\n\n### `LocationProvider`\n\nA context provider that provides the current location to its children. This is required for the router to function.\n\nProps:\n\n  - `scope?: string | RegExp` - Sets a scope for the paths that the router will handle (intercept). If a path does not match the scope, either by starting with the provided string or matching the RegExp, the router will ignore it and default browser navigation will apply.\n\nTypically, you would wrap your entire app in this provider:\n\n```js\nimport { LocationProvider } from 'preact-iso';\n\nconst App = () => (\n    <LocationProvider scope=\"/app\">\n        {/* Your app here */}\n    </LocationProvider>\n);\n```\n\n#### Restore default browser navigation for one link\n\nThe location provider intercepts interactions upon anchor tags (`<a />`) to facilitate single-page app navigation. Some interactions, however, are ignored to avoid hindering users as well as giving devs ways to opt-out and resume standard browser behavior. The rules for ignoring are as such:\n\n1. You hold a modifier key (ctrl, meta, alt, shift) or click a mouse button other than the main one.\n2. Cross-origin links, e.g., you're on `https://example.org` but the link points to `https://example.com`.\n3. The link points to a fragment identifier on the current page, e.g., to `#section`.\n4. The link's `target` is set to anything but `_self` (such as `_blank` or `_top`) -- this is a convenient way to opt-out.\n5. The link points outside the `scope` of your `LocationProvider`.\n6. The link has a `download` attribute.\n\n### `Router`\n\nProps:\n\n  - `onRouteChange?: (url: string) => void` - Callback to be called when a route changes.\n  - `onLoadStart?: (url: string) => void` - Callback to be called when a route starts loading (i.e., if it suspends). This will not be called before navigations to sync routes or subsequent navigations to async routes.\n  - `onLoadEnd?: (url: string) => void` - Callback to be called after a route finishes loading (i.e., if it suspends). This will not be called after navigations to sync routes or subsequent navigations to async routes.\n\n```js\nimport { LocationProvider, Router } from 'preact-iso';\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<Router\n\t\t\tonRouteChange={(url) => console.log('Route changed to', url)}\n\t\t\tonLoadStart={(url) => console.log('Starting to load', url)}\n\t\t\tonLoadEnd={(url) => console.log('Finished loading', url)}\n\t\t>\n\t\t\t<Home path=\"/\" />\n\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t<Profile path=\"/profile/:id\" />\n\t\t</Router>\n\t</LocationProvider>\n);\n```\n\n### `Route`\n\n\nThere are two ways to define routes using `preact-iso`:\n\n1. Append router params to the route components directly: `<Home path=\"/\" />`\n2. Use the `Route` component instead: `<Route path=\"/\" component={Home} />`\n\nAppending arbitrary props to components not unreasonable in JavaScript, as JS is a dynamic language that's perfectly happy to support dynamic & arbitrary interfaces. However, TypeScript, which many of us use even when writing JS (via TS's language server), is not exactly a fan of this sort of interface design.\n\nTS does not (yet) allow for overriding a child's props from the parent component so we cannot, for instance, define `<Home>` as taking no props _unless_ it's a child of a `<Router>`, in which case it can have a `path` prop. This leaves us with a bit of a dilemma: either we define all of our routes as taking `path` props so we don't see TS errors when writing `<Home path=\"/\" />` or we create wrapper components to handle the route definitions.\n\nWhile `<Home path=\"/\" />` is completely equivalent to `<Route path=\"/\" component={Home} />`, TS users may find the latter preferable.\n\n```js\nimport { LocationProvider, Router, Route } from 'preact-iso';\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<Router>\n\t\t\t{/* Both of these are equivalent */}\n\t\t\t<Home path=\"/\" />\n\t\t\t<Route path=\"/\" component={Home} />\n\n\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t<Profile path=\"/profile/:id\" />\n\t\t\t<NotFound default />\n\t\t</Router>\n\t</LocationProvider>\n);\n```\n\nProps for any route component:\n\n  - `path: string` - The path to match (read on)\n  - `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches\n\nSpecific to the `Route` component:\n\n  - `component: AnyComponent` - The component to render when the route matches\n\n#### Path Segment Matching\n\nPaths are matched using a simple string matching algorithm. The following features may be used:\n\n  - `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`)\n    - `/profile/:id` will match `/profile/123` and `/profile/abc`\n    - `/profile/:id?` will match `/profile` and `/profile/123`\n    - `/profile/:id*` will match `/profile`, `/profile/123`, and `/profile/123/abc`\n    - `/profile/:id+` will match `/profile/123`, `/profile/123/abc`\n  - `*` - Matches one or more URL segments\n    - `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc.\n\nThese can then be composed to create more complex routes:\n\n  - `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc.\n\nThe difference between `/:id*` and `/:id/*` is that in the former, the `id` param will include the entire path after it, while in the latter, the `id` is just the single path segment.\n\n  - `/profile/:id*`, with `/profile/123/abc`\n    - `id` is `123/abc`\n  - `/profile/:id/*`, with `/profile/123/abc`\n    - `id` is `123`\n\nYou can narrow prop types for your routes using `RoutePropsForPath<path>`:\n```ts\nimport type { RoutePropsForPath } from 'preact-iso'\n\nfunction User(props: RoutePropsForPath<'/user/:id'>) {\n\tprops.user.id2 // type error\n\tprops.user.id // no type error\n}\n```\n\n### `useLocation`\n\nA hook to work with the `LocationProvider` to access location context.\n\nReturns an object with the following properties:\n\n  - `url: string` - The current path & search params\n  - `path: string` - The current path\n  - `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)\n  - `route: (url: string, replace?: boolean) => void` - A function to programmatically navigate to a new route. The `replace` param can optionally be used to overwrite history, navigating them away without keeping the current location in the history stack.\n  - `back: () => void` - A function to programmatically navigate back one entry in the browser history stack.\n  - `forward: () => void` - A function to programmatically navigate forward one entry in the browser history stack.\n\n### `useRoute`\n\nA hook to access current route information. Unlike `useLocation`, this hook only works within `<Router>` components.\n\nReturns an object with the following properties:\n\n  - `path: string` - The current path\n  - `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)\n  - `params: Record<string, string>` - The current route parameters (`/profile/:id` -> `{ id: '123' }`)\n\n### `lazy`\n\nMake a lazily-loaded version of a Component.\n\n`lazy()` takes an async function that resolves to a Component, and returns a wrapper version of that Component. The wrapper component can be rendered right away, even though the component is only loaded the first time it is rendered.\n\n```js\nimport { lazy, LocationProvider, Router } from 'preact-iso';\n\n// Synchronous, not code-splitted:\nimport Home from './routes/home.js';\n\n// Asynchronous, code-splitted:\nconst Profiles = lazy(() => import('./routes/profiles.js').then(m => m.Profiles)); // Expects a named export called `Profiles`\nconst Profile = lazy(() => import('./routes/profile.js')); // Expects a default export\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<Router>\n\t\t\t<Home path=\"/\" />\n\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t<Profile path=\"/profile/:id\" />\n\t\t</Router>\n\t</LocationProvider>\n);\n```\n\nThe result of `lazy()` also exposes a `preload()` method that can be used to load the component before it's needed for rendering. Entirely optional, but can be useful on focus, mouse over, etc. to start loading the component a bit earlier than it otherwise would be.\n\n```js\nconst Profile = lazy(() => import('./routes/profile.js'));\n\nfunction Home() {\n    return (\n        <a href=\"/profile/rschristian\" onMouseOver={() => Profile.preload()}>\n            Profile Page -- Hover over me to preload the module!\n        </a>\n    );\n}\n```\n\n### `ErrorBoundary`\n\nA simple component to catch errors in the component tree below it.\n\nProps:\n\n  - `onError?: (error: Error) => void` - A callback to be called when an error is caught\n\n```js\nimport { LocationProvider, ErrorBoundary, Router } from 'preact-iso';\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<ErrorBoundary onError={(e) => console.log(e)}>\n\t\t\t<Router>\n\t\t\t\t<Home path=\"/\" />\n\t\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t\t<Profile path=\"/profile/:id\" />\n\t\t\t</Router>\n\t\t</ErrorBoundary>\n\t</LocationProvider>\n);\n```\n\n### `hydrate`\n\nA thin wrapper around Preact's `hydrate` export, it switches between hydrating and rendering the provided element, depending on whether the current page has been prerendered. Additionally, it checks to ensure it's running in a browser context before attempting any rendering, making it a no-op during SSR.\n\nPairs with the `prerender()` function.\n\nParams:\n\n  - `jsx: ComponentChild` - The JSX element or component to render\n  - `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided.\n\n```js\nimport { hydrate } from 'preact-iso';\n\nconst App = () => (\n\t<div class=\"app\">\n\t\t<h1>Hello World</h1>\n\t</div>\n);\n\nhydrate(<App />);\n```\n\nHowever, it is just a simple utility method. By no means is it essential to use, you can always use Preact's `hydrate` export directly.\n\n### `prerender`\n\nRenders a Virtual DOM tree to an HTML string using `preact-render-to-string`. The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external (either no `target` or something other than `target=\"_self\"`) URL strings found in links on the generated page.\n\nPairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration)'s prerendering.\n\nParams:\n\n  - `jsx: ComponentChild` - The JSX element or component to render\n\n```js\nimport { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso';\n\n// Asynchronous (throws a promise)\nconst Foo = lazy(() => import('./foo.js'));\nconst Bar = lazy(() => import('./bar.js'));\n\nconst App = () => (\n\t<LocationProvider>\n\t\t<ErrorBoundary>\n\t\t\t<Router>\n\t\t\t\t<Foo path=\"/\" />\n\t\t\t\t<Bar path=\"/bar\" />\n\t\t\t</Router>\n\t\t</ErrorBoundary>\n\t</LocationProvider>\n);\n\nconst { html, links } = await prerender(<App />);\n```\n\n### `locationStub`\n\nA utility function to imitate the `location` object in a non-browser environment. Our router relies upon this to function, so if you are using `preact-iso` outside of a browser context and are not prerendering via `@preact/preset-vite` (which does this for you), you can use this utility to set a stubbed `location` object.\n\n```js\nimport { locationStub } from 'preact-iso/prerender';\n\nlocationStub('/foo/bar?baz=qux#quux');\n\nconsole.log(location.pathname); // \"/foo/bar\"\n```\n\n## Navigation API Entry Docs\n\nThe Navigation API is a new web standard that provides an updated method of handling \"navigation\" in web applications, supporting SPA-style routing as a first-class citizen. The older History API can be wrangled to support this and has been the standard for many years, but the Navigation API provides a much more robust set of tools that are really, really attractive for routers like `preact-iso` to take advantage of.\n\nWhilst the API [sees fairly wide support](https://caniuse.com/wf-navigation), it is still newly available and thus may not be viable for some targets. As such, we've provided a new entry point that will allow you to take advantage of this API if you wish, but the default remains targetting the History API. The Navigation API entry point is available at `preact-iso/router/navigation-api`.\n\n### Differences in usage\n\nThe differences lie entirely within the [`useLocation()`](#uselocation) hook: instead of returning a `route()` function, you use the global `navigation` object to perform all navigations.\n\nThe [`navigation` object](https://developer.mozilla.org/en-US/docs/Web/API/Navigation) contains many of the useful utilities that go along with a router, like `.forward()`, `.back()`, `.canGoForward()`, `.canGoBack()`, `.entries()`, etc. It actually offers far more utilities than the base router did, and does so with less library code overall, so if you have access to it, it's a really nice upgrade.\n\n## License\n\n[MIT](./LICENSE)\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"noEmit\": true,\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"skipLibCheck\": false,\n    \"jsx\": \"react\",\n    \"jsxFactory\": \"h\",\n    \"jsxFragmentFactory\": \"Fragment\",\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"preact-iso\",\n\t\"version\": \"2.12.0\",\n\t\"type\": \"module\",\n\t\"main\": \"src/index.js\",\n\t\"module\": \"src/index.js\",\n\t\"types\": \"src/index.d.ts\",\n\t\"exports\": {\n\t\t\".\": \"./src/index.js\",\n\t\t\"./router\": \"./src/router.js\",\n\t\t\"./router/navigation-api\": \"./src/router-navigation-api.js\",\n\t\t\"./lazy\": \"./src/lazy.js\",\n\t\t\"./prerender\": \"./src/prerender.js\",\n\t\t\"./hydrate\": \"./src/hydrate.js\"\n\t},\n\t\"sideEffects\": false,\n\t\"license\": \"MIT\",\n\t\"description\": \"Isomorphic utilities for Preact\",\n\t\"author\": \"The Preact Authors (https://preactjs.com)\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/preactjs/preact-iso.git\"\n\t},\n\t\"files\": [\n\t\t\"src\",\n\t\t\"!src/internal.d.ts\",\n\t\t\"LICENSE\",\n\t\t\"package.json\",\n\t\t\"README.md\"\n\t],\n\t\"scripts\": {\n\t\t\"test\": \"npm run test:node && npm run test:browser\",\n\t\t\"test:browser\": \"wtr test/*.test.js\",\n\t\t\"test:node\": \"uvu test/node\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"preact\": \">=10 || >= 11.0.0-0\",\n\t\t\"preact-render-to-string\": \">=6.4.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@types/mocha\": \"^10.0.7\",\n\t\t\"@types/sinon-chai\": \"^3.2.12\",\n\t\t\"@web/dev-server-esbuild\": \"^1.0.2\",\n\t\t\"@web/test-runner\": \"^0.18.3\",\n\t\t\"chai\": \"^5.1.1\",\n\t\t\"htm\": \"^3.1.1\",\n\t\t\"kleur\": \"^4.1.5\",\n\t\t\"navigation-api-types\": \"^0.6.1\",\n\t\t\"preact\": \"^10.26.5\",\n\t\t\"preact-render-to-string\": \"^6.6.1\",\n\t\t\"sinon\": \"^18.0.0\",\n\t\t\"sinon-chai\": \"^4.0.0\",\n\t\t\"uvu\": \"^0.5.6\"\n\t},\n\t\"overrides\": {\n\t\t\"@web/dev-server-core\": \"0.7.1\"\n\t}\n}"
  },
  {
    "path": "polyglot-utils/.gitignore",
    "content": "# Language-specific files to ignore\n__pycache__/\n*.pyc\n.venv/\n.ruby-version\n"
  },
  {
    "path": "polyglot-utils/README.md",
    "content": "# Preact ISO URL Pattern Matching - Polyglot Utils\n\nMulti-language implementations of URL pattern matching utilities for building bespoke server setups that need to preload JS/CSS resources or handle early 404 responses.\n\n## Use Case\n\nThis utility is designed for server languages that **cannot do SSR/prerendering** but still want to provide better experiences. It enables servers to:\n\n- **Add preload head tags** for JS,CSS before serving HTML\n- **Return early 404 pages** for unmatched routes\n- **Generate dynamic titles** based on route parameters\n\n## How can I implement preloading of JS, CSS?\n\nTypical implementation flow:\n\n1. **Build-time Setup:**\n   - Write your routes as an array in a JS file\n   - Create a build script that exports route patterns and entry files to a `.json` file\n   - Configure your frontend build tool to output a `manifest` file mapping entry files to final fingerprinted/hashed output JS/CSS files and dependencies\n\n2. **Server-time Processing:**\n   - Load the JSON route file when a request comes in\n   - Match the requested URL against each route pattern until you find a match\n   - Once matched, you have the source entry `.jsx` file\n   - Load the build manifest file to find which JS chunk contains that code and its dependency files\n   - Generate `<link rel=\"preload\">` tags for each dependency (JS, CSS, images, icons)\n   - Inject those head tags into the HTML before serving\n\n3. **Result:**\n   - Browsers start downloading critical resources immediately\n   - Faster page loads without full SSR complexity\n   - Early 404s for invalid routes\n\n### Example - preloading of JS, CSS\n\nHere's how you might integrate this into a server setup. Let's say you have a client side `routes.js` as follows:\n\n```js\nimport { lazy } from 'preact-iso';\n\nexport const routes = [\n  {\n    \"path\": \"/users/:userId/posts\",\n    \"component\": lazy(() => import(\"pages/UserPosts.jsx\")),\n    \"title\": \"Posts by :userId\"\n  },\n  {\n    \"path\": \"/products/:category/:id\",\n    \"component\": lazy(() => import(\"pages/Product.jsx\")),\n    \"title\": \"Product :id\"\n  }\n];\n```\n\n1. **Generate Routes JSON (routes.json)**\n\nYou can use the following standalone node.js script to create `routes.json` during build (you could convert it into a plugin for your frontend build tool):\n```js\nconst routeDir = path.resolve(__dirname, 'client/src/routes');\nlet routesFile = fs.readFileSync(path.resolve(routeDir, 'routes.js'), 'utf-8');\nroutesFile = routesFile.replace(/lazy\\s*\\(\\s*\\(\\s*\\)\\s*=>\\s*import\\s*\\(\\s*(.+)\\s*\\)\\s*\\)\\s*(,?)/g, '$1$2');\nconst fileName = path.resolve(__dirname, 'routes-temp.js')\nfs.writeFileSync(fileName, routesFile, 'utf-8');\nconst routes = (await import(fileName)).default;\nfs.unlinkSync(fileName);\nconst routeInfo = routes.map((route) => ({\n  path: route.path,\n  title: typeof route.title === 'string' ? route.title : null,\n  Component: path.relative(__dirname, path.resolve(routeDir, `${route.Component}.jsx`)),\n  default: route.default,\n}));\n// console.log(routeInfo);\nfs.writeFileSync(\n  path.resolve(__dirname, 'dist/routes.json'),\n  JSON.stringify(routeInfo, null, 2),\n  'utf-8'\n);\n```\n\nThe script produces the following `routes.json` file:\n```json\n[\n  {\n    \"path\": \"/users/:userId/posts\",\n    \"component\": \"pages/UserPosts.jsx\",\n    \"title\": \"Posts by :userId\"\n  },\n  {\n    \"path\": \"/products/:category/:id\",\n    \"component\": \"pages/Product.jsx\",\n    \"title\": \"Product :id\"\n  }\n]\n```\n\n2. **Build Manifest (manifest.json)**\n\nThis is the file your client build tool generates. Check your build tool's documentation for exact format. Below is an example with few important fields from a Vite manifest file:\n```json\n{\n  \"pages/UserPosts.jsx\": {\n    \"file\": \"assets/UserPosts-abc123.js\",\n    \"css\": [\"assets/UserPosts-def456.css\"],\n    \"imports\": [\"chunks/shared-ghi789.js\"]\n  }\n}\n```\n\n3. **Server Implementation**\n```python\n# Python example\nimport json\n\nroutes = json.load(open('../dist/routes.json'))\nmanifest = json.load(open('../dist/.vite/manifest.json'))\n\ndef handle_request(url_path):\n    for route in routes:\n        matches = preact_iso_url_pattern_match(url_path, route['path'])\n        if matches:\n            # Generate preload tags\n            component = route['component']\n            entry_info = manifest[component]\n\n            preload_tags = []\n            for js_file in [entry_info['file']] + entry_info.get('imports', []):\n                preload_tags.append(f'<link rel=\"modulepreload\" crossorigin href=\"{js_file}\">')\n\n            for css_file in entry_info.get('css', []):\n                preload_tags.append(f'<link rel=\"stylesheet\" crossorigin href=\"{css_file}\">')\n\n            # Generate dynamic title\n            title = route['title']\n            for param, value in matches['params'].items():\n                title = title.replace(f':{param}', value)\n\n            return {\n                'preload_tags': preload_tags,\n                'title': title,\n                'params': matches['params']\n            }\n\n    # No match found - return early 404\n    return None\n```\n\nThis approach gives you the performance benefits of resource preloading without the complexity of full server-side rendering.\n\n## Available Languages\n\nGo, PHP, Python and Ruby.\n\nFind the corresponding language's sub-directory. Each language has a README that contains usage examples and API reference.\n\n## Running Tests\n\n```bash\n# Run all tests across all languages\n./run_tests.sh\n\n# Or run individual language tests\ncd go && go test -v\ncd python && python3 test_preact_iso_url_pattern.py\ncd ruby && ruby test_preact_iso_url_pattern.rb\ncd php && php test_preact_iso_url_pattern.php\n```\n"
  },
  {
    "path": "polyglot-utils/go/README.md",
    "content": "# Go Implementation\n\nURL pattern matching utility for Go servers.\n\n## Setup\n\nCode tested on Go 1.24.x.\n\n```sh\n# If using in a project, initialize go module\ngo mod init myproject\n# No third party dependencies needed. Just run the tests or use the function directly\n```\n\n## Running Tests\n\n```sh\ngo test -v\n```\n\n## Usage\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n    matches := preactIsoUrlPatternMatch(\"/users/test%40example.com/posts\", \"/users/:userId/posts\", nil)\n    if matches != nil {\n        fmt.Printf(\"User ID: %s\\n\", matches.Params[\"userId\"]) // Output: test@example.com\n    }\n}\n```\n\n## Function Signature\n\n```go\nfunc preactIsoUrlPatternMatch(url, route string, matches *Matches) *Matches\n```\n\n### Parameters\n\n- `url` (string): The URL path to match\n- `route` (string): The route pattern with parameters\n- `matches` (*Matches): Optional pre-existing matches to extend\n\n### Return Value\n\nReturns a `*Matches` struct on success, or `nil` if no match:\n\n```go\ntype Matches struct {\n    Params map[string]string\n    Rest   string\n}\n```\n\n## Route Patterns\n\n| Pattern | Description | Example |\n|---------|-------------|---------|\n| `/users/:id` | Named parameter | `{id: \"123\"}` |\n| `/users/:id?` | Optional parameter | `{id: \"\"}` |\n| `/files/:path+` | Required rest parameter | `{path: \"docs/readme.txt\"}` |\n| `/static/:path*` | Optional rest parameter | `{path: \"css/main.css\"}` |\n| `/static/*` | Anonymous wildcard | `{Rest: \"/images/logo.png\"}` |\n"
  },
  {
    "path": "polyglot-utils/go/go.mod",
    "content": "module myproject\n\ngo 1.13\n"
  },
  {
    "path": "polyglot-utils/go/preact_iso_url_pattern.go",
    "content": "// Run program: go run preact-iso-url-pattern.go\n\npackage main\n\nimport (\n\t// \"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype Matches struct {\n\tParams map[string]string `json:\"params\"`\n\tRest   string            `json:\"rest,omitempty\"`\n}\n\nfunc preactIsoUrlPatternMatch(urlStr, route string, matches *Matches) *Matches {\n\tif matches == nil {\n\t\tmatches = &Matches{\n\t\t\tParams: make(map[string]string),\n\t\t}\n\t}\n\turlParts := filterEmpty(strings.Split(urlStr, \"/\"))\n\trouteParts := filterEmpty(strings.Split(route, \"/\"))\n\n\tfor i := 0; i < max(len(urlParts), len(routeParts)); i++ {\n\t\tvar m, param, flag string\n\t\tif i < len(routeParts) {\n\t\t\tre := regexp.MustCompile(`^(:?)(.*?)([+*?]?)$`)\n\t\t\tmatches := re.FindStringSubmatch(routeParts[i])\n\t\t\tif len(matches) > 3 {\n\t\t\t\tm, param, flag = matches[1], matches[2], matches[3]\n\t\t\t}\n\t\t}\n\n\t\tvar val string\n\t\tif i < len(urlParts) {\n\t\t\tval = urlParts[i]\n\t\t}\n\n\t\t// segment match:\n\t\tif m == \"\" && param != \"\" && param == val {\n\t\t\tcontinue\n\t\t}\n\n\t\t// /foo/* match\n\t\tif m == \"\" && val != \"\" && flag == \"*\" {\n\t\t\tmatches.Rest = \"/\" + strings.Join(urlParts[i:], \"/\")\n\t\t\tbreak\n\t\t}\n\n\t\t// segment mismatch / missing required field:\n\t\tif m == \"\" || (val == \"\" && flag != \"?\" && flag != \"*\") {\n\t\t\treturn nil\n\t\t}\n\n\t\trest := flag == \"+\" || flag == \"*\"\n\n\t\t// rest (+/*) match:\n\t\tif rest {\n\t\t\tdecodedParts := make([]string, len(urlParts[i:]))\n\t\t\tfor j, part := range urlParts[i:] {\n\t\t\t\tdecoded, err := url.QueryUnescape(part)\n\t\t\t\tif err != nil {\n\t\t\t\t\tdecoded = part // fallback to original if decode fails\n\t\t\t\t}\n\t\t\t\tdecodedParts[j] = decoded\n\t\t\t}\n\t\t\tval = strings.Join(decodedParts, \"/\")\n\t\t} else if val != \"\" {\n\t\t\t// normal/optional field: decode val (like JavaScript does)\n\t\t\tdecoded, err := url.QueryUnescape(val)\n\t\t\tif err != nil {\n\t\t\t\tdecoded = urlParts[i]\n\t\t\t}\n\t\t\tval = decoded\n\t\t}\n\n\t\tmatches.Params[param] = val\n\n\t\tif rest {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn matches\n}\n\nfunc filterEmpty(s []string) []string {\n\tvar result []string\n\tfor _, str := range s {\n\t\tif str != \"\" {\n\t\t\tresult = append(result, str)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc max(a, b int) int {\n\tif a > b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// Example usage:\n// func main() {\n// \tparams := &Matches{Params: make(map[string]string)}\n// \tfmt.Println(preactIsoUrlPatternMatch(\"/foo/bar%20baz\", \"/foo/:param\", params))\n//\n// \tparams := &Matches{Params: make(map[string]string)}\n// \tfmt.Println(preactIsoUrlPatternMatch(\"/foo/bar/baz\", \"/foo/*\"))\n//\n// \tparams := &Matches{Params: make(map[string]string)}\n// \tfmt.Println(preactIsoUrlPatternMatch(\"/foo\", \"/foo/:param?\"))\n//\n// \tparams := &Matches{Params: make(map[string]string)}\n// \tfmt.Println(preactIsoUrlPatternMatch(\"/foo/bar\", \"/bar/:param\"))\n//\n// \tparams := &Matches{Params: make(map[string]string)}\n// \tfmt.Println(preactIsoUrlPatternMatch(\"/users/test%40example.com/posts\", \"/users/:userId/posts\"))\n// }\n"
  },
  {
    "path": "polyglot-utils/go/preact_iso_url_pattern_test.go",
    "content": "package main\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// Note 1: This is different from JS implementation. Here it is empty string and not nil\n// This was intentionally implemented this way, but we can change if it's a problem\n\nfunc TestPreactIsoUrlPatternMatch(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\turl      string\n\t\troute    string\n\t\tmatches  *Matches\n\t\texpected *Matches\n\t}{\n\t\t// Base route tests\n\t\t{\n\t\t\tname:     \"Base route - exact match\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/\",\n\t\t\tmatches:  nil,\n\t\t\texpected: &Matches{Params: map[string]string{}},\n\t\t},\n\t\t{\n\t\t\tname:     \"Base route - no match\",\n\t\t\turl:      \"/user/1\",\n\t\t\troute:    \"/\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Param route tests\n\t\t{\n\t\t\tname:    \"Param route - match\",\n\t\t\turl:     \"/user/2\",\n\t\t\troute:   \"/user/:id\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Param route - no match\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/user/:id\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Rest segment tests\n\t\t{\n\t\t\tname:    \"Rest segment - match\",\n\t\t\turl:     \"/user/foo\",\n\t\t\troute:   \"/user/*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{},\n\t\t\t\tRest:   \"/foo\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Rest segment - match multiple segments\",\n\t\t\turl:     \"/user/foo/bar/baz\",\n\t\t\troute:   \"/user/*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{},\n\t\t\t\tRest:   \"/foo/bar/baz\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Rest segment - no match\",\n\t\t\turl:      \"/user\",\n\t\t\troute:    \"/user/*\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Param route with rest segment\n\t\t{\n\t\t\tname:    \"Param with rest - single segment\",\n\t\t\turl:     \"/user/2/foo\",\n\t\t\troute:   \"/user/:id/*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"2\"},\n\t\t\t\tRest:   \"/foo\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Param with rest - multiple segments\",\n\t\t\turl:     \"/user/2/foo/bar/bob\",\n\t\t\troute:   \"/user/:id/*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"2\"},\n\t\t\t\tRest:   \"/foo/bar/bob\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Param with rest - no match\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/user/:id/*\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Optional param tests\n\t\t{\n\t\t\tname:    \"Optional param - empty\",\n\t\t\turl:     \"/user\",\n\t\t\troute:   \"/user/:id?\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\t// Check \"Note 1\" at the top of the file\n\t\t\t\tParams: map[string]string{\"id\": \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Optional param - no match base\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/user/:id?\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Optional rest param tests (/:x*)\n\t\t{\n\t\t\tname:    \"Optional rest param - empty\",\n\t\t\turl:     \"/user\",\n\t\t\troute:   \"/user/:id*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\t// Check \"Note 1\" at the top of the file\n\t\t\t\tParams: map[string]string{\"id\": \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Optional rest param - with segments\",\n\t\t\turl:     \"/user/foo/bar\",\n\t\t\troute:   \"/user/:id*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"foo/bar\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Optional param - no match base\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/user/:id*\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Required rest param tests (/:x+)\n\t\t{\n\t\t\tname:    \"Required rest param - single segment\",\n\t\t\turl:     \"/user/foo\",\n\t\t\troute:   \"/user/:id+\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"foo\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Required rest param - multiple segments\",\n\t\t\turl:     \"/user/foo/bar\",\n\t\t\troute:   \"/user/:id+\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"foo/bar\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Required rest param - empty (should fail)\",\n\t\t\turl:      \"/user\",\n\t\t\troute:    \"/user/:id+\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"Required rest param - root mismatch\",\n\t\t\turl:      \"/\",\n\t\t\troute:    \"/user/:id+\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\n\t\t// Leading/trailing slashes\n\t\t{\n\t\t\tname:    \"Leading/trailing slashes\",\n\t\t\turl:     \"/about-late/_SEGMENT1_/_SEGMENT2_/\",\n\t\t\troute:   \"/about-late/:seg1/:seg2/\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\n\t\t\t\t\t\"seg1\": \"_SEGMENT1_\",\n\t\t\t\t\t\"seg2\": \"_SEGMENT2_\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Additional tests that are not in test/node/router-match.test.js\n\t\t// URL encoding tests\n\t\t{\n\t\t\tname:    \"URL encoded param\",\n\t\t\turl:     \"/foo/bar%20baz\",\n\t\t\troute:   \"/foo/:param\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"param\": \"bar baz\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"URL encoded email in param\",\n\t\t\turl:     \"/users/test%40example.com/posts\",\n\t\t\troute:   \"/users/:userId/posts\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"userId\": \"test@example.com\"},\n\t\t\t},\n\t\t},\n\n\t\t// Complex rest segment with encoding\n\t\t{\n\t\t\tname:    \"Rest segment with encoded parts\",\n\t\t\turl:     \"/api/path/with%20spaces/and%2Fslashes\",\n\t\t\troute:   \"/api/:path+\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"path\": \"path/with spaces/and/slashes\"},\n\t\t\t},\n\t\t},\n\n\t\t// Edge cases\n\t\t{\n\t\t\tname:     \"Empty route\",\n\t\t\turl:      \"/foo\",\n\t\t\troute:    \"\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty url with param\",\n\t\t\turl:      \"\",\n\t\t\troute:    \"/:param\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n      name:    \"Mixed required and optional params\",\n      url:     \"/foo/bar\",\n      route:   \"/:required/:optional?\",\n      matches: nil,\n      expected: &Matches{\n        Params: map[string]string{\"required\": \"foo\", \"optional\": \"bar\"},\n      },\n    },\n    {\n      name:    \"Mixed required and optional params - missing optional\",\n      url:     \"/foo\",\n      route:   \"/:required/:optional?\",\n      matches: nil,\n      expected: &Matches{\n        Params: map[string]string{\"required\": \"foo\", \"optional\": \"\"},\n      },\n    },\n\n\t\t// Test with pre-existing matches\n\t\t{\n\t\t\tname:    \"Pre-existing matches object\",\n\t\t\turl:     \"/foo/bar\",\n\t\t\troute:   \"/:first/:second\",\n\t\t\tmatches: &Matches{Params: map[string]string{\"existing\": \"value\"}},\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\n\t\t\t\t\t\"existing\": \"value\",\n\t\t\t\t\t\"first\":    \"foo\",\n\t\t\t\t\t\"second\":   \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Complex nested paths\n\t\t{\n\t\t\tname:    \"Complex nested path with multiple params\",\n\t\t\turl:     \"/api/v1/users/123/posts/456/comments\",\n\t\t\troute:   \"/api/:version/users/:userId/posts/:postId/comments\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\n\t\t\t\t\t\"version\": \"v1\",\n\t\t\t\t\t\"userId\":  \"123\",\n\t\t\t\t\t\"postId\":  \"456\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Test case where route is longer than URL\n\t\t{\n\t\t\tname:     \"Route longer than URL - required param missing\",\n\t\t\turl:      \"/api\",\n\t\t\troute:    \"/api/:version/:resource\",\n\t\t\tmatches:  nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"Route longer than URL - optional param\",\n\t\t\turl:     \"/api\",\n\t\t\troute:   \"/api/:version?\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"version\": \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Multiple slashes in URL should be normalized\",\n\t\t\turl:     \"//user//123//\",\n\t\t\troute:   \"/user/:id\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"123\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Route with multiple slashes\",\n\t\t\turl:     \"/user/123\",\n\t\t\troute:   \"//user//:id//\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"id\": \"123\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Complex URL encoding in rest params\",\n\t\t\turl:     \"/files/folder%2Fsubfolder/file%20name.txt\",\n\t\t\troute:   \"/files/:path+\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"path\": \"folder/subfolder/file name.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Special characters encoded in URL\",\n\t\t\turl:     \"/search/query%3F%2B%23%26test\",\n\t\t\troute:   \"/search/:query\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"query\": \"query?+#&test\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Unicode characters encoded\",\n\t\t\turl:     \"/user/Jos%C3%A9\",\n\t\t\troute:   \"/user/:name\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{\"name\": \"José\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Empty segments in middle of URL\",\n\t\t\turl:     \"/api//v1//users\",\n\t\t\troute:   \"/api/v1/users\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Route with only wildcards\",\n\t\t\turl:     \"/anything/goes/here\",\n\t\t\troute:   \"*\",\n\t\t\tmatches: nil,\n\t\t\texpected: &Matches{\n\t\t\t\tParams: map[string]string{},\n\t\t\t\tRest:   \"/anything/goes/here\",\n\t\t\t},\n\t\t},\n\t}\n\n\t// URL decoding error handling tests\n\turlDecodingTests := []struct {\n\t\tname     string\n\t\turl      string\n\t\troute    string\n\t\tmatches  *Matches\n\t}{\n\t\t{\n\t\t\tname:    \"Malformed percent encoding in simple param - should not crash\",\n\t\t\turl:     \"/user/test%\",\n\t\t\troute:   \"/user/:id\",\n\t\t\tmatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"Malformed percent encoding in rest param - should not crash\",\n\t\t\turl:     \"/files/test%/file\",\n\t\t\troute:   \"/files/:path+\",\n\t\t\tmatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"Invalid unicode sequence - should not crash\",\n\t\t\turl:     \"/user/test%C3\",\n\t\t\troute:   \"/user/:id\",\n\t\t\tmatches: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := preactIsoUrlPatternMatch(tt.url, tt.route, tt.matches)\n\n\t\t\tif tt.expected == nil {\n\t\t\t\tif result != nil {\n\t\t\t\t\tt.Errorf(\"Expected nil, got %+v\", result)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif result == nil {\n\t\t\t\tt.Errorf(\"Expected %+v, got nil\", tt.expected)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Detailed debugging for failing tests\n\t\t\tif !reflect.DeepEqual(result.Params, tt.expected.Params) {\n\t\t\t\tt.Errorf(\"Params mismatch for url=%q route=%q\", tt.url, tt.route)\n\t\t\t\tt.Errorf(\"  Expected: %+v\", tt.expected.Params)\n\t\t\t\tt.Errorf(\"  Got:      %+v\", result.Params)\n\n\t\t\t\t// Additional debug info for rest param cases\n\t\t\t\tif strings.Contains(tt.route, \"+\") || strings.Contains(tt.route, \"*\") {\n\t\t\t\t\turlParts := filterEmpty(strings.Split(tt.url, \"/\"))\n\t\t\t\t\trouteParts := filterEmpty(strings.Split(tt.route, \"/\"))\n\t\t\t\t\tt.Errorf(\"  Debug: urlParts=%v, routeParts=%v\", urlParts, routeParts)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check rest\n\t\t\tif result.Rest != tt.expected.Rest {\n\t\t\t\tt.Errorf(\"Rest mismatch. Expected %q, got %q\", tt.expected.Rest, result.Rest)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Test URL decoding error handling - these should not crash\n\tfor _, tt := range urlDecodingTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// The main requirement is that this doesn't crash\n\t\t\t// We don't care about the exact return value as long as it doesn't panic\n\t\t\tresult := preactIsoUrlPatternMatch(tt.url, tt.route, tt.matches)\n\t\t\t// Should either work or return nil, but not crash\n\t\t\tif result != nil {\n\t\t\t\t// If it returns a result, just verify it has params\n\t\t\t\tif result.Params == nil {\n\t\t\t\t\tt.Errorf(\"Result should have non-nil Params map\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Debug helper to trace execution\nfunc TestDebugSpecificCase(t *testing.T) {\n\tt.Run(\"Debug rest param multiple segments\", func(t *testing.T) {\n\t\turl := \"/user/foo/bar\"\n\t\troute := \"/user/:id*\"\n\n\t\tt.Logf(\"Testing: url=%q route=%q\", url, route)\n\n\t\t// Manual trace of what should happen:\n\t\turlParts := filterEmpty(strings.Split(url, \"/\"))\n\t\trouteParts := filterEmpty(strings.Split(route, \"/\"))\n\n\t\tt.Logf(\"urlParts: %v\", urlParts)\n\t\tt.Logf(\"routeParts: %v\", routeParts)\n\t\tt.Logf(\"Expected at i=1: should take urlParts[1:] = %v\", urlParts[1:])\n\t\tt.Logf(\"Expected result: %q\", strings.Join(urlParts[1:], \"/\"))\n\n\t\tresult := preactIsoUrlPatternMatch(url, route, nil)\n\t\tif result != nil {\n\t\t\tt.Logf(\"Actual result: %+v\", result)\n\t\t} else {\n\t\t\tt.Logf(\"Actual result: nil\")\n\t\t}\n\t})\n}\n\nfunc TestFilterEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tinput:    []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"no empty strings\",\n\t\t\tinput:    []string{\"a\", \"b\", \"c\"},\n\t\t\texpected: []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"with empty strings\",\n\t\t\tinput:    []string{\"\", \"a\", \"\", \"b\", \"\"},\n\t\t\texpected: []string{\"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"all empty strings\",\n\t\t\tinput:    []string{\"\", \"\", \"\"},\n\t\t\texpected: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterEmpty(tt.input)\n\t\t\t// Handle nil slice vs empty slice comparison\n\t\t\tif len(result) == 0 && len(tt.expected) == 0 {\n\t\t\t\treturn // Both are effectively empty\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Expected %+v, got %+v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "polyglot-utils/php/README.md",
    "content": "# PHP Implementation\n\nURL pattern matching utility for PHP servers.\n\n## Setup\n\nCode tested on PHP 8.3.x.\n\n```sh\nphp --version  # Ensure PHP 7.0+ is available\n# No third party dependencies needed. Just run the tests or use the function directly\n```\n\n## Running Tests\n\n```sh\nphp test_preact_iso_url_pattern.php\n```\n\n## Usage\n\n```php\n<?php\nrequire_once 'preact-iso-url-pattern.php';\n\n$matches = preactIsoUrlPatternMatch(\"/users/test%40example.com/posts\", \"/users/:userId/posts\");\nif ($matches) {\n    echo \"User ID: \" . $matches['params']->userId . \"\\n\";  // Output: test@example.com\n}\n?>\n```\n\n## Function Signature\n\n```php\nfunction preactIsoUrlPatternMatch($url, $route, $matches = null): array|null\n```\n\n### Parameters\n\n- `$url` (string): The URL path to match\n- `$route` (string): The route pattern with parameters\n- `$matches` (array, optional): Pre-existing matches array to extend\n\n### Return Value\n\nReturns an array on success, or `null` if no match:\n\n```php\n[\n    'params' => (object)['userId' => '123'],\n    'userId' => '123',\n    'rest' => '/additional/path'  // Optional\n]\n```\n\n**Note**: The `params` property is a PHP object (stdClass) to maintain consistency with JSON serialization, while the outer structure is an array.\n\n## Route Patterns\n\n| Pattern | Description | Example Result |\n|---------|-------------|----------------|\n| `/users/:id` | Named parameter | `['params' => (object)['id' => '123'], 'id' => '123']` |\n| `/users/:id?` | Optional parameter | `['params' => (object)['id' => null], 'id' => null]` |\n| `/files/:path+` | Required rest parameter | `['params' => (object)['path' => 'docs/readme.txt']]` |\n| `/static/:path*` | Optional rest parameter | `['params' => (object)['path' => 'css/main.css']]` |\n| `/static/*` | Anonymous wildcard | `['params' => (object)[], 'rest' => '/images/logo.png']` |\n"
  },
  {
    "path": "polyglot-utils/php/preact-iso-url-pattern.php",
    "content": "<?php\n// Run program: php preact-iso-url-pattern.php\n\n// Safe URL decode function with error handling\nfunction safeUrldecode($str) {\n    if ($str === null || $str === '') {\n        return $str;\n    }\n\n    // urldecode in PHP generally doesn't throw exceptions,\n    // but we can add validation for malformed percent encoding\n    $decoded = urldecode($str);\n\n    // If the original contained a % but decoding didn't change much,\n    // it might be malformed, but PHP's urldecode is quite tolerant\n    return $decoded;\n}\n\nfunction preactIsoUrlPatternMatch($url, $route, $matches = null) {\n    if ($matches === null) {\n        $matches = ['params' => (object)[]];\n    }\n    $url = array_values(array_filter(explode('/', $url)));\n    $route = array_values(array_filter(explode('/', $route ?? '')));\n\n    for ($i = 0; $i < max(count($url), count($route)); $i++) {\n        preg_match('/^(:?)(.*?)([+*?]?)$/', $route[$i] ?? '', $parts);\n        $m = $parts[1] ?? '';\n        $param = $parts[2] ?? '';\n        $flag = $parts[3] ?? '';\n        $val = $url[$i] ?? null;\n\n        // segment match:\n        if (!$m && $param === $val) continue;\n\n        // /foo/* match\n        if (!$m && $val && $flag == '*') {\n            $decodedParts = array_map('safeUrldecode', array_slice($url, $i));\n            $matches['rest'] = '/' . implode('/', $decodedParts);\n            break;\n        }\n\n        // segment mismatch / missing required field:\n        if (!$m || (!$val && $flag != '?' && $flag != '*')) {\n            return null;\n        }\n        $rest = $flag == '+' || $flag == '*';\n\n        // rest (+/*) match:\n        if ($rest) {\n            $decodedParts = array_map('safeUrldecode', array_slice($url, $i));\n            $val = implode('/', $decodedParts) ?: null;\n        }\n        // normal/optional field:\n        elseif ($val) {\n            $val = safeUrldecode($url[$i]);\n        }\n\n        $matches['params']->$param = $val;\n        if (!isset($matches[$param])) {\n            $matches[$param] = $val;\n        }\n\n        if ($rest) break;\n    }\n\n    return $matches;\n}\n// Example usage:\n// var_dump(preactIsoUrlPatternMatch(\"/foo/bar%20baz\", \"/foo/:param\"));\n// var_dump(preactIsoUrlPatternMatch(\"/foo/bar/baz\", \"/foo/*\"));\n// var_dump(preactIsoUrlPatternMatch(\"/foo\", \"/foo/:param?\"));\n// var_dump(preactIsoUrlPatternMatch(\"/foo/bar\", \"/bar/:param\"));\n// var_dump(preactIsoUrlPatternMatch('/users/test%40example.com/posts', '/users/:userId/posts'));\n?>\n"
  },
  {
    "path": "polyglot-utils/php/test_preact_iso_url_pattern.php",
    "content": "<?php\n/**\n * Test suite for preact-iso-url-pattern.php - ported from Go tests\n * Run with: php test_preact_iso_url_pattern.php\n */\n\nrequire_once 'preact-iso-url-pattern.php';\n\nclass TestPreactIsoUrlPatternMatch {\n    private $tests = 0;\n    private $passed = 0;\n    private $failed = 0;\n\n    public function run() {\n        echo \"Running PHP tests for preact-iso-url-pattern...\\n\\n\";\n\n        // Run all test methods\n        $methods = get_class_methods($this);\n        foreach ($methods as $method) {\n            if (strpos($method, 'test_') === 0) {\n                $this->runTest($method);\n            }\n        }\n\n        echo \"\\n\" . str_repeat(\"=\", 60) . \"\\n\";\n        echo \"Test Results: {$this->passed} passed, {$this->failed} failed, {$this->tests} total\\n\";\n\n        if ($this->failed > 0) {\n            exit(1);\n        }\n        echo \"All tests passed!\\n\";\n    }\n\n    private function runTest($methodName) {\n        $this->tests++;\n        try {\n            $this->$methodName();\n            $this->passed++;\n            echo \".\";\n        } catch (Exception $e) {\n            $this->failed++;\n            echo \"F\";\n            echo \"\\nFAILED: $methodName - \" . $e->getMessage() . \"\\n\";\n        }\n    }\n\n    private function assertEqual($expected, $actual, $message = '') {\n        // Use JSON comparison for deep equality check (works for arrays and objects)\n        $expectedJson = json_encode($expected);\n        $actualJson = json_encode($actual);\n\n        if ($expectedJson !== $actualJson) {\n            $expectedStr = json_encode($expected, JSON_PRETTY_PRINT);\n            $actualStr = json_encode($actual, JSON_PRETTY_PRINT);\n            throw new Exception(\"Expected:\\n$expectedStr\\nGot:\\n$actualStr\\n$message\");\n        }\n    }\n\n    private function assertNull($actual, $message = '') {\n        if ($actual !== null) {\n            $actualStr = json_encode($actual, JSON_PRETTY_PRINT);\n            throw new Exception(\"Expected null, got:\\n$actualStr\\n$message\");\n        }\n    }\n\n    private function assertNotNull($actual, $message = '') {\n        if ($actual === null) {\n            throw new Exception(\"Expected non-null value, got null\\n$message\");\n        }\n    }\n\n    // Test methods start here\n\n    // Base route tests\n    public function test_base_route_exact_match() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/\");\n        $expected = ['params' => (object)[]];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_base_route_no_match() {\n        $result = preactIsoUrlPatternMatch(\"/user/1\", \"/\");\n        $this->assertNull($result);\n    }\n\n    // Param route tests\n    public function test_param_route_match() {\n        $result = preactIsoUrlPatternMatch(\"/user/2\", \"/user/:id\");\n        $expected = ['params' => (object)['id' => '2'], 'id' => '2'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_param_route_no_match() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id\");\n        $this->assertNull($result);\n    }\n\n    // Rest segment tests\n    public function test_rest_segment_match() {\n        $result = preactIsoUrlPatternMatch(\"/user/foo\", \"/user/*\");\n        $expected = ['params' => (object)[], 'rest' => '/foo'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_rest_segment_match_multiple_segments() {\n        $result = preactIsoUrlPatternMatch(\"/user/foo/bar/baz\", \"/user/*\");\n        $expected = ['params' => (object)[], 'rest' => '/foo/bar/baz'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_rest_segment_no_match() {\n        $result = preactIsoUrlPatternMatch(\"/user\", \"/user/*\");\n        $this->assertNull($result);\n    }\n\n    public function test_rest_segment_no_match_different_case() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id/*\");\n        $this->assertNull($result);\n    }\n\n    // Param route with rest segment\n    public function test_param_with_rest_single_segment() {\n        $result = preactIsoUrlPatternMatch(\"/user/2/foo\", \"/user/:id/*\");\n        $expected = ['params' => (object)['id' => '2'], 'id' => '2', 'rest' => '/foo'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_param_with_rest_multiple_segments() {\n        $result = preactIsoUrlPatternMatch(\"/user/2/foo/bar/bob\", \"/user/:id/*\");\n        $expected = ['params' => (object)['id' => '2'], 'id' => '2', 'rest' => '/foo/bar/bob'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_param_with_rest_no_match() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id/*\");\n        $this->assertNull($result);\n    }\n\n    // Optional param tests\n    public function test_optional_param_empty() {\n        $result = preactIsoUrlPatternMatch(\"/user\", \"/user/:id?\");\n        $expected = ['params' => (object)['id' => null], 'id' => null];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_optional_param_no_match_base() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id?\");\n        $this->assertNull($result);\n    }\n\n    // Optional rest param tests (/:x*)\n    public function test_optional_rest_param_empty() {\n        $result = preactIsoUrlPatternMatch(\"/user\", \"/user/:id*\");\n        $expected = ['params' => (object)['id' => null], 'id' => null];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_optional_rest_param_with_segments() {\n        $result = preactIsoUrlPatternMatch(\"/user/foo/bar\", \"/user/:id*\");\n        $expected = ['params' => (object)['id' => 'foo/bar'], 'id' => 'foo/bar'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_optional_param_no_match_base_duplicate() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id*\");\n        $this->assertNull($result);\n    }\n\n    // Required rest param tests (/:x+)\n    public function test_required_rest_param_single_segment() {\n        $result = preactIsoUrlPatternMatch(\"/user/foo\", \"/user/:id+\");\n        $expected = ['params' => (object)['id' => 'foo'], 'id' => 'foo'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_required_rest_param_multiple_segments() {\n        $result = preactIsoUrlPatternMatch(\"/user/foo/bar\", \"/user/:id+\");\n        $expected = ['params' => (object)['id' => 'foo/bar'], 'id' => 'foo/bar'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_required_rest_param_empty_should_fail() {\n        $result = preactIsoUrlPatternMatch(\"/user\", \"/user/:id+\");\n        $this->assertNull($result);\n    }\n\n    public function test_required_rest_param_root_mismatch() {\n        $result = preactIsoUrlPatternMatch(\"/\", \"/user/:id+\");\n        $this->assertNull($result);\n    }\n\n    // Leading/trailing slashes\n    public function test_leading_trailing_slashes() {\n        $result = preactIsoUrlPatternMatch(\"/about-late/_SEGMENT1_/_SEGMENT2_/\", \"/about-late/:seg1/:seg2/\");\n        $expected = ['params' => (object)['seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_'], 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_'];\n        $this->assertEqual($expected, $result);\n    }\n\n    // Additional tests that are not in test/node/router-match.test.js\n    // URL encoding tests\n    public function test_url_encoded_param() {\n        $result = preactIsoUrlPatternMatch(\"/foo/bar%20baz\", \"/foo/:param\");\n        $expected = ['params' => (object)['param' => 'bar baz'], 'param' => 'bar baz'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_url_encoded_email_in_param() {\n        $result = preactIsoUrlPatternMatch(\"/users/test%40example.com/posts\", \"/users/:userId/posts\");\n        $expected = ['params' => (object)['userId' => 'test@example.com'], 'userId' => 'test@example.com'];\n        $this->assertEqual($expected, $result);\n    }\n\n    // Complex rest segment with encoding\n    public function test_rest_segment_with_encoded_parts() {\n        $result = preactIsoUrlPatternMatch(\"/api/path/with%20spaces/and%2Fslashes\", \"/api/:path+\");\n        $expected = ['params' => (object)['path' => 'path/with spaces/and/slashes'], 'path' => 'path/with spaces/and/slashes'];\n        $this->assertEqual($expected, $result);\n    }\n\n    // Edge cases\n    public function test_empty_route() {\n        $result = preactIsoUrlPatternMatch(\"/foo\", \"\");\n        $this->assertNull($result);\n    }\n\n    public function test_empty_url_with_param() {\n        $result = preactIsoUrlPatternMatch(\"\", \"/:param\");\n        $this->assertNull($result);\n    }\n\n    public function test_mixed_required_and_optional_params() {\n        $result = preactIsoUrlPatternMatch(\"/foo/bar\", \"/:required/:optional?\");\n        $expected = ['params' => (object)['required' => 'foo', 'optional' => 'bar'], 'required' => 'foo', 'optional' => 'bar'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_mixed_required_and_optional_params_missing_optional() {\n        $result = preactIsoUrlPatternMatch(\"/foo\", \"/:required/:optional?\");\n        $expected = ['params' => (object)['required' => 'foo', 'optional' => null], 'required' => 'foo', 'optional' => null];\n        $this->assertEqual($expected, $result);\n    }\n\n    // Test with pre-existing matches\n    public function test_pre_existing_matches_object() {\n        $matches = ['params' => (object)['existing' => 'value']];\n        $result = preactIsoUrlPatternMatch(\"/foo/bar\", \"/:first/:second\", $matches);\n        $expected = ['params' => (object)['existing' => 'value', 'first' => 'foo', 'second' => 'bar'], 'first' => 'foo', 'second' => 'bar'];\n        $this->assertEqual($expected, $result);\n    }\n\n\n    // Complex nested paths\n    public function test_complex_nested_path_with_multiple_params() {\n        $result = preactIsoUrlPatternMatch(\"/api/v1/users/123/posts/456/comments\", \"/api/:version/users/:userId/posts/:postId/comments\");\n        $expected = [\n            'params' => (object)['version' => 'v1', 'userId' => '123', 'postId' => '456'],\n            'version' => 'v1', 'userId' => '123', 'postId' => '456'\n        ];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_route_longer_than_url_required_param_missing() {\n        $result = preactIsoUrlPatternMatch(\"/api\", \"/api/:version/:resource\");\n        $this->assertNull($result);\n    }\n\n    public function test_route_longer_than_url_optional_param() {\n        $result = preactIsoUrlPatternMatch(\"/api\", \"/api/:version?\");\n        $expected = ['params' => (object)['version' => null], 'version' => null];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_multiple_slashes_in_url_should_be_normalized() {\n        $result = preactIsoUrlPatternMatch(\"//user//123//\", \"/user/:id\");\n        $expected = ['params' => (object)['id' => '123'], 'id' => '123'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_route_with_multiple_slashes() {\n        $result = preactIsoUrlPatternMatch(\"/user/123\", \"//user//:id//\");\n        $expected = ['params' => (object)['id' => '123'], 'id' => '123'];\n        $this->assertEqual($expected, $result);\n    }\n\n\n    // Additional URL encoding tests\n    public function test_complex_url_encoding_in_rest_params() {\n        $result = preactIsoUrlPatternMatch(\"/files/folder%2Fsubfolder/file%20name.txt\", \"/files/:path+\");\n        $expected = ['params' => (object)['path' => 'folder/subfolder/file name.txt'], 'path' => 'folder/subfolder/file name.txt'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_special_characters_encoded_in_url() {\n        $result = preactIsoUrlPatternMatch(\"/search/query%3F%2B%23%26test\", \"/search/:query\");\n        $expected = ['params' => (object)['query' => 'query?+#&test'], 'query' => 'query?+#&test'];\n        $this->assertEqual($expected, $result);\n    }\n\n\n\n    public function test_unicode_characters_encoded() {\n        $result = preactIsoUrlPatternMatch(\"/user/Jos%C3%A9\", \"/user/:name\");\n        $expected = ['params' => (object)['name' => 'José'], 'name' => 'José'];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_empty_segments_in_middle_of_url() {\n        $result = preactIsoUrlPatternMatch(\"/api//v1//users\", \"/api/v1/users\");\n        $expected = ['params' => (object)[]];\n        $this->assertEqual($expected, $result);\n    }\n\n    public function test_route_with_only_wildcards() {\n        $result = preactIsoUrlPatternMatch(\"/anything/goes/here\", \"*\");\n        $expected = ['params' => (object)[], 'rest' => '/anything/goes/here'];\n        $this->assertEqual($expected, $result);\n    }\n\n    // URL decoding error handling tests\n    public function test_malformed_percent_encoding_simple_param() {\n        // Test malformed percent encoding in simple param - should not crash\n        $result = preactIsoUrlPatternMatch(\"/user/test%\", \"/user/:id\");\n        // Should either work or return null, but not crash\n        $this->assertNotNull($result);\n    }\n\n    public function test_malformed_percent_encoding_rest_param() {\n        // Test malformed percent encoding in rest param - should not crash\n        $result = preactIsoUrlPatternMatch(\"/files/test%/file\", \"/files/:path+\");\n        // Should either work or return null, but not crash\n        $this->assertNotNull($result);\n    }\n\n    public function test_invalid_unicode_sequence() {\n        // Test invalid unicode sequence - should not crash\n        $result = preactIsoUrlPatternMatch(\"/user/test%C3\", \"/user/:id\");\n        // Should either work or return null, but not crash\n        $this->assertNotNull($result);\n    }\n}\n\n// Run tests if this file is executed directly\nif (php_sapi_name() === 'cli') {\n    $test = new TestPreactIsoUrlPatternMatch();\n    $test->run();\n}\n?>\n"
  },
  {
    "path": "polyglot-utils/python/README.md",
    "content": "# Python Implementation\n\nURL pattern matching utility for Python servers.\n\n## Setup\n\nCode tested on Python 3.12.x.\n\nNo external dependencies required - uses only Python standard library.\n\n```sh\npython3 --version  # Ensure Python 3.6+ is available\n# No third party dependencies needed. Just run the tests or use the function directly\n```\n\n## Running Tests\n\n```sh\npython3 test_preact_iso_url_pattern.py\n```\n\n## Usage\n\n```python\nfrom preact_iso_url_pattern import preact_iso_url_pattern_match\n\nmatches = preact_iso_url_pattern_match(\"/users/test%40example.com/posts\", \"/users/:userId/posts\")\nif matches:\n    print(f\"User ID: {matches['params']['userId']}\")  # Output: test@example.com\n```\n\n## Function Signature\n\n```python\ndef preact_iso_url_pattern_match(url, route, matches=None) -> dict | None\n```\n\n### Parameters\n\n- `url` (str): The URL path to match\n- `route` (str): The route pattern with parameters\n- `matches` (dict, optional): Pre-existing matches dictionary to extend\n\n### Return Value\n\nReturns a dictionary on success, or `None` if no match:\n\n```python\n{\n    \"params\": {\"userId\": \"123\"},\n    \"userId\": \"123\",\n    \"rest\": \"/additional/path\"  # Optional\n}\n```\n\n## Route Patterns\n\n| Pattern | Description | Example Result |\n|---------|-------------|----------------|\n| `/users/:id` | Named parameter | `{\"params\": {\"id\": \"123\"}, \"id\": \"123\"}` |\n| `/users/:id?` | Optional parameter | `{\"params\": {\"id\": None}, \"id\": None}` |\n| `/files/:path+` | Required rest parameter | `{\"params\": {\"path\": \"docs/readme.txt\"}}` |\n| `/static/:path*` | Optional rest parameter | `{\"params\": {\"path\": \"css/main.css\"}}` |\n| `/static/*` | Anonymous wildcard | `{\"params\": {}, \"rest\": \"/images/logo.png\"}` |\n"
  },
  {
    "path": "polyglot-utils/python/preact_iso_url_pattern.py",
    "content": "# Run program: python3 preact-iso-url-pattern.py\n\nfrom urllib.parse import unquote\n\n# Safe URL decode function with error handling\ndef safe_unquote(s):\n    if s is None or s == '':\n        return s\n    try:\n        return unquote(s)\n    except UnicodeDecodeError:\n        # If unquote fails due to malformed encoding, return original string\n        return s\n\ndef preact_iso_url_pattern_match(url, route, matches=None):\n    # Initialize matches object if not provided\n    if matches is None:\n        matches = {'params': {}}\n    url = list(filter(None, url.split('/')))\n    route = list(filter(None, (route or '').split('/')))\n\n    for i in range(max(len(url), len(route))):\n        m, param, flag = '', '', ''\n        if i < len(route):\n            parts = route[i].split(':')\n            m = ':' if len(parts) > 1 else ''\n            param = parts[-1]\n            flag = ''\n            if param and param[-1] in '+*?':\n                flag = param[-1]\n                param = param[:-1]\n\n        val = url[i] if i < len(url) else None\n\n        # segment match:\n        if not m and param == val:\n            continue\n\n        # /foo/* match\n        if not m and val and flag == '*':\n            # Store remaining path segments in rest\n            matches['rest'] = '/' + '/'.join(map(safe_unquote, url[i:]))\n            break\n\n        # segment mismatch / missing required field:\n        if not m or (not val and flag != '?' and flag != '*'):\n            return None\n\n        rest = flag in ('+', '*')\n\n        # rest (+/*) match:\n        if rest:\n            val = '/'.join(map(safe_unquote, url[i:])) or None\n        # normal/optional field:\n        elif val:\n            val = safe_unquote(val)\n\n        # Store parameter values in matches\n        matches['params'][param] = val\n        if param not in matches:\n            matches[param] = val\n\n        if rest:\n            break\n\n    return matches\n\n# Example usage:\n# print(preact_iso_url_pattern_match(\"/foo/bar%20baz\", \"/foo/:param\"))\n# print(preact_iso_url_pattern_match(\"/foo/bar/baz\", \"/foo/*\"))\n# print(preact_iso_url_pattern_match(\"/foo\", \"/foo/:param?\"))\n# print(preact_iso_url_pattern_match(\"/foo/bar\", \"/bar/:param\"))\n# print(preact_iso_url_pattern_match('/users/test%40example.com/posts', '/users/:userId/posts'))"
  },
  {
    "path": "polyglot-utils/python/test_preact_iso_url_pattern.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Test suite for preact_iso_url_pattern.py - ported from Go tests\"\"\"\n\nimport unittest\nfrom preact_iso_url_pattern import preact_iso_url_pattern_match\n\n\nclass TestPreactIsoUrlPatternMatch(unittest.TestCase):\n\n    # Base route tests\n    def test_base_route_exact_match(self):\n        \"\"\"Base route - exact match\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/\")\n        expected = {'params': {}}\n        self.assertEqual(result, expected)\n\n    def test_base_route_no_match(self):\n        \"\"\"Base route - no match\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/1\", \"/\")\n        self.assertIsNone(result)\n\n    # Param route tests\n    def test_param_route_match(self):\n        \"\"\"Param route - match\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/2\", \"/user/:id\")\n        expected = {'params': {'id': '2'}, 'id': '2'}\n        self.assertEqual(result, expected)\n\n    def test_param_route_no_match(self):\n        \"\"\"Param route - no match\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id\")\n        self.assertIsNone(result)\n\n    # Rest segment tests\n    def test_rest_segment_match(self):\n        \"\"\"Rest segment - match\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/foo\", \"/user/*\")\n        expected = {'params': {}, 'rest': '/foo'}\n        self.assertEqual(result, expected)\n\n    def test_rest_segment_match_multiple_segments(self):\n        \"\"\"Rest segment - match multiple segments\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/foo/bar/baz\", \"/user/*\")\n        expected = {'params': {}, 'rest': '/foo/bar/baz'}\n        self.assertEqual(result, expected)\n\n    def test_rest_segment_no_match(self):\n        \"\"\"Rest segment - no match\"\"\"\n        result = preact_iso_url_pattern_match(\"/user\", \"/user/*\")\n        self.assertIsNone(result)\n\n    def test_rest_segment_no_match_different_case(self):\n        \"\"\"Rest segment - no match different case\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id/*\")\n        self.assertIsNone(result)\n\n    # Param route with rest segment\n    def test_param_with_rest_single_segment(self):\n        \"\"\"Param with rest - single segment\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/2/foo\", \"/user/:id/*\")\n        expected = {'params': {'id': '2'}, 'id': '2', 'rest': '/foo'}\n        self.assertEqual(result, expected)\n\n    def test_param_with_rest_multiple_segments(self):\n        \"\"\"Param with rest - multiple segments\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/2/foo/bar/bob\", \"/user/:id/*\")\n        expected = {'params': {'id': '2'}, 'id': '2', 'rest': '/foo/bar/bob'}\n        self.assertEqual(result, expected)\n\n    def test_param_with_rest_no_match(self):\n        \"\"\"Param with rest - no match\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id/*\")\n        self.assertIsNone(result)\n\n    # Optional param tests\n    def test_optional_param_empty(self):\n        \"\"\"Optional param - empty\"\"\"\n        result = preact_iso_url_pattern_match(\"/user\", \"/user/:id?\")\n        expected = {'params': {'id': None}, 'id': None}\n        self.assertEqual(result, expected)\n\n    def test_optional_param_no_match_base(self):\n        \"\"\"Optional param - no match base\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id?\")\n        self.assertIsNone(result)\n\n    # Optional rest param tests (/:x*)\n    def test_optional_rest_param_empty(self):\n        \"\"\"Optional rest param - empty\"\"\"\n        result = preact_iso_url_pattern_match(\"/user\", \"/user/:id*\")\n        expected = {'params': {'id': None}, 'id': None}\n        self.assertEqual(result, expected)\n\n    def test_optional_rest_param_with_segments(self):\n        \"\"\"Optional rest param - with segments\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/foo/bar\", \"/user/:id*\")\n        expected = {'params': {'id': 'foo/bar'}, 'id': 'foo/bar'}\n        self.assertEqual(result, expected)\n\n    def test_optional_param_no_match_base_duplicate(self):\n        \"\"\"Optional param - no match base duplicate\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id*\")\n        self.assertIsNone(result)\n\n    # Required rest param tests (/:x+)\n    def test_required_rest_param_single_segment(self):\n        \"\"\"Required rest param - single segment\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/foo\", \"/user/:id+\")\n        expected = {'params': {'id': 'foo'}, 'id': 'foo'}\n        self.assertEqual(result, expected)\n\n    def test_required_rest_param_multiple_segments(self):\n        \"\"\"Required rest param - multiple segments\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/foo/bar\", \"/user/:id+\")\n        expected = {'params': {'id': 'foo/bar'}, 'id': 'foo/bar'}\n        self.assertEqual(result, expected)\n\n    def test_required_rest_param_empty_should_fail(self):\n        \"\"\"Required rest param - empty (should fail)\"\"\"\n        result = preact_iso_url_pattern_match(\"/user\", \"/user/:id+\")\n        self.assertIsNone(result)\n\n    def test_required_rest_param_root_mismatch(self):\n        \"\"\"Required rest param - root mismatch\"\"\"\n        result = preact_iso_url_pattern_match(\"/\", \"/user/:id+\")\n        self.assertIsNone(result)\n\n    # Leading/trailing slashes\n    def test_leading_trailing_slashes(self):\n        \"\"\"Leading/trailing slashes\"\"\"\n        result = preact_iso_url_pattern_match(\"/about-late/_SEGMENT1_/_SEGMENT2_/\", \"/about-late/:seg1/:seg2/\")\n        expected = {'params': {'seg1': '_SEGMENT1_', 'seg2': '_SEGMENT2_'}, 'seg1': '_SEGMENT1_', 'seg2': '_SEGMENT2_'}\n        self.assertEqual(result, expected)\n\n    # Additional tests that are not in test/node/router-match.test.js\n    # URL encoding tests\n    def test_url_encoded_param(self):\n        \"\"\"URL encoded param\"\"\"\n        result = preact_iso_url_pattern_match(\"/foo/bar%20baz\", \"/foo/:param\")\n        expected = {'params': {'param': 'bar baz'}, 'param': 'bar baz'}\n        self.assertEqual(result, expected)\n\n    def test_url_encoded_email_in_param(self):\n        \"\"\"URL encoded email in param\"\"\"\n        result = preact_iso_url_pattern_match(\"/users/test%40example.com/posts\", \"/users/:userId/posts\")\n        expected = {'params': {'userId': 'test@example.com'}, 'userId': 'test@example.com'}\n        self.assertEqual(result, expected)\n\n    # Complex rest segment with encoding\n    def test_rest_segment_with_encoded_parts(self):\n        \"\"\"Rest segment with encoded parts\"\"\"\n        result = preact_iso_url_pattern_match(\"/api/path/with%20spaces/and%2Fslashes\", \"/api/:path+\")\n        expected = {'params': {'path': 'path/with spaces/and/slashes'}, 'path': 'path/with spaces/and/slashes'}\n        self.assertEqual(result, expected)\n\n    # Edge cases\n    def test_empty_route(self):\n        \"\"\"Empty route\"\"\"\n        result = preact_iso_url_pattern_match(\"/foo\", \"\")\n        self.assertIsNone(result)\n\n    def test_empty_url_with_param(self):\n        \"\"\"Empty url with param\"\"\"\n        result = preact_iso_url_pattern_match(\"\", \"/:param\")\n        self.assertIsNone(result)\n\n    def test_mixed_required_and_optional_params(self):\n        \"\"\"Mixed required and optional params\"\"\"\n        result = preact_iso_url_pattern_match(\"/foo/bar\", \"/:required/:optional?\")\n        expected = {'params': {'required': 'foo', 'optional': 'bar'}, 'required': 'foo', 'optional': 'bar'}\n        self.assertEqual(result, expected)\n\n    def test_mixed_required_and_optional_params_missing_optional(self):\n        \"\"\"Mixed required and optional params - missing optional\"\"\"\n        result = preact_iso_url_pattern_match(\"/foo\", \"/:required/:optional?\")\n        expected = {'params': {'required': 'foo', 'optional': None}, 'required': 'foo', 'optional': None}\n        self.assertEqual(result, expected)\n\n    # Test with pre-existing matches\n    def test_pre_existing_matches_object(self):\n        \"\"\"Pre-existing matches object\"\"\"\n        matches = {'params': {'existing': 'value'}}\n        result = preact_iso_url_pattern_match(\"/foo/bar\", \"/:first/:second\", matches)\n        expected = {'params': {'existing': 'value', 'first': 'foo', 'second': 'bar'}, 'first': 'foo', 'second': 'bar'}\n        self.assertEqual(result, expected)\n\n    # Complex nested paths\n    def test_complex_nested_path_with_multiple_params(self):\n        \"\"\"Complex nested path with multiple params\"\"\"\n        result = preact_iso_url_pattern_match(\"/api/v1/users/123/posts/456/comments\", \"/api/:version/users/:userId/posts/:postId/comments\")\n        expected = {\n            'params': {'version': 'v1', 'userId': '123', 'postId': '456'},\n            'version': 'v1', 'userId': '123', 'postId': '456'\n        }\n        self.assertEqual(result, expected)\n\n    def test_route_longer_than_url_required_param_missing(self):\n        \"\"\"Route longer than URL - required param missing\"\"\"\n        result = preact_iso_url_pattern_match(\"/api\", \"/api/:version/:resource\")\n        self.assertIsNone(result)\n\n    def test_route_longer_than_url_optional_param(self):\n        \"\"\"Route longer than URL - optional param\"\"\"\n        result = preact_iso_url_pattern_match(\"/api\", \"/api/:version?\")\n        expected = {'params': {'version': None}, 'version': None}\n        self.assertEqual(result, expected)\n\n    def test_multiple_slashes_in_url_should_be_normalized(self):\n        \"\"\"Multiple slashes in URL should be normalized\"\"\"\n        result = preact_iso_url_pattern_match(\"//user//123//\", \"/user/:id\")\n        expected = {'params': {'id': '123'}, 'id': '123'}\n        self.assertEqual(result, expected)\n\n    def test_route_with_multiple_slashes(self):\n        \"\"\"Route with multiple slashes\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/123\", \"//user//:id//\")\n        expected = {'params': {'id': '123'}, 'id': '123'}\n        self.assertEqual(result, expected)\n\n    def test_complex_url_encoding_in_rest_params(self):\n        \"\"\"Complex URL encoding in rest params\"\"\"\n        result = preact_iso_url_pattern_match(\"/files/folder%2Fsubfolder/file%20name.txt\", \"/files/:path+\")\n        expected = {'params': {'path': 'folder/subfolder/file name.txt'}, 'path': 'folder/subfolder/file name.txt'}\n        self.assertEqual(result, expected)\n\n    def test_special_characters_encoded_in_url(self):\n        \"\"\"Special characters encoded in URL\"\"\"\n        result = preact_iso_url_pattern_match(\"/search/query%3F%2B%23%26test\", \"/search/:query\")\n        expected = {'params': {'query': 'query?+#&test'}, 'query': 'query?+#&test'}\n        self.assertEqual(result, expected)\n\n    def test_unicode_characters_encoded(self):\n        \"\"\"Unicode characters encoded\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/Jos%C3%A9\", \"/user/:name\")\n        expected = {'params': {'name': 'José'}, 'name': 'José'}\n        self.assertEqual(result, expected)\n\n    def test_empty_segments_in_middle_of_url(self):\n        \"\"\"Empty segments in middle of URL\"\"\"\n        result = preact_iso_url_pattern_match(\"/api//v1//users\", \"/api/v1/users\")\n        expected = {'params': {}}\n        self.assertEqual(result, expected)\n\n    def test_route_with_only_wildcards(self):\n        \"\"\"Route with only wildcards\"\"\"\n        result = preact_iso_url_pattern_match(\"/anything/goes/here\", \"*\")\n        expected = {'params': {}, 'rest': '/anything/goes/here'}\n        self.assertEqual(result, expected)\n\n\nclass TestUrlDecodingErrorHandling(unittest.TestCase):\n    \"\"\"Tests specifically for URL decoding error scenarios\"\"\"\n\n    def test_malformed_percent_encoding_simple_param(self):\n        \"\"\"Test malformed percent encoding in simple param - should not crash\"\"\"\n        # This should handle malformed encoding gracefully\n        result = preact_iso_url_pattern_match(\"/user/test%\", \"/user/:id\")\n        # Should either work or return None, but not crash\n        self.assertIsNotNone(result)\n\n    def test_malformed_percent_encoding_rest_param(self):\n        \"\"\"Test malformed percent encoding in rest param - should not crash\"\"\"\n        result = preact_iso_url_pattern_match(\"/files/test%/file\", \"/files/:path+\")\n        # Should either work or return None, but not crash\n        self.assertIsNotNone(result)\n\n    def test_invalid_unicode_sequence(self):\n        \"\"\"Test invalid unicode sequence - should not crash\"\"\"\n        result = preact_iso_url_pattern_match(\"/user/test%C3\", \"/user/:id\")\n        # Should either work or return None, but not crash\n        self.assertIsNotNone(result)\n\n\nif __name__ == '__main__':\n    # Run tests with verbose output\n    unittest.main(verbosity=2)\n"
  },
  {
    "path": "polyglot-utils/ruby/README.md",
    "content": "# Ruby Implementation\n\nURL pattern matching utility for Ruby servers.\n\n## Setup\n\nCode tested on ruby 3.2.x.\n\n```sh\nruby --version  # Ensure Ruby 2.0+ is available\n# No third party dependencies needed. Just run the tests or use the function directly\n```\n\n## Running Tests\n\n```sh\nruby test_preact_iso_url_pattern.rb\n```\n\n## Usage\n\n```ruby\nrequire_relative 'preact-iso-url-pattern'\n\nmatches = preact_iso_url_pattern_match(\"/users/test%40example.com/posts\", \"/users/:userId/posts\")\nif matches\n  puts \"User ID: #{matches['params']['userId']}\"  # Output: test@example.com\nend\n```\n\n## Function Signature\n\n```ruby\ndef preact_iso_url_pattern_match(url, route, matches = nil) -> Hash | nil\n```\n\n### Parameters\n\n- `url` (String): The URL path to match\n- `route` (String): The route pattern with parameters\n- `matches` (Hash, optional): Pre-existing matches hash to extend\n\n### Return Value\n\nReturns a Hash on success, or `nil` if no match:\n\n```ruby\n{\n  'params' => { 'userId' => '123' },\n  'userId' => '123',\n  'rest' => '/additional/path'  # Optional\n}\n```\n\n## Route Patterns\n\n| Pattern | Description | Example Result |\n|---------|-------------|----------------|\n| `/users/:id` | Named parameter | `{'params' => {'id' => '123'}, 'id' => '123'}` |\n| `/users/:id?` | Optional parameter | `{'params' => {'id' => nil}, 'id' => nil}` |\n| `/files/:path+` | Required rest parameter | `{'params' => {'path' => 'docs/readme.txt'}}` |\n| `/static/:path*` | Optional rest parameter | `{'params' => {'path' => 'css/main.css'}}` |\n| `/static/*` | Anonymous wildcard | `{'params' => {}, 'rest' => '/images/logo.png'}` |\n"
  },
  {
    "path": "polyglot-utils/ruby/preact-iso-url-pattern.rb",
    "content": "# Run program: ruby preact-iso-url-pattern.rb\nrequire 'cgi'\n\n# Safe URL decode function with error handling\ndef safe_cgi_unescape(str)\n  return str if str.nil? || str.empty?\n\n  begin\n    CGI.unescape(str)\n  rescue ArgumentError\n    # If CGI.unescape fails due to malformed encoding, return original string\n    str\n  end\nend\n\ndef preact_iso_url_pattern_match(url, route, matches = nil)\n  matches ||= { 'params' => {} }\n  url = url.split('/').reject(&:empty?)\n  route = (route || '').split('/').reject(&:empty?)\n\n  (0...[url.length, route.length].max).each do |i|\n    m, param, flag = route[i]&.match(/^(:?)(.*?)([+*?]?)$/)&.captures || ['', '', '']\n    val = url[i]\n\n    # segment match:\n    next if m.empty? && param == val\n\n    # /foo/* match\n    if m.empty? && val && flag == '*'\n      decoded_parts = url[i..].map { |part| safe_cgi_unescape(part) }\n      matches['rest'] = '/' + decoded_parts.join('/')\n      break\n    end\n\n    # segment mismatch / missing required field:\n    return nil if m.empty? || (!val && flag != '?' && flag != '*')\n\n    rest = flag == '+' || flag == '*'\n\n    # rest (+/*) match:\n    if rest\n      decoded_parts = url[i..].map { |part| safe_cgi_unescape(part) }\n      joined = decoded_parts.join('/')\n      val = joined.empty? ? nil : joined\n    # normal/optional field:\n    elsif val\n      val = safe_cgi_unescape(val)\n    end\n\n    matches['params'][param] = val\n    matches[param] = val unless matches.key?(param)\n\n    break if rest\n  end\n\n  matches\nend\n\n# Example usage:\n# puts preact_iso_url_pattern_match(\"/foo/bar%20baz\", \"/foo/:param\")\n# puts preact_iso_url_pattern_match(\"/foo/bar/baz\", \"/foo/*\")\n# puts preact_iso_url_pattern_match(\"/foo\", \"/foo/:param?\")\n# puts preact_iso_url_pattern_match(\"/foo/bar\", \"/bar/:param\")\n# puts preact_iso_url_pattern_match('/users/test%40example.com/posts', '/users/:userId/posts')\n"
  },
  {
    "path": "polyglot-utils/ruby/test_preact_iso_url_pattern.rb",
    "content": "#!/usr/bin/env ruby\n# Test suite for preact-iso-url-pattern.rb - ported from Go tests\n\nrequire 'minitest/autorun'\nrequire_relative 'preact-iso-url-pattern'\n\nclass TestPreactIsoUrlPatternMatch < Minitest::Test\n\n  # Base route tests\n  def test_base_route_exact_match\n    # Base route - exact match\n    result = preact_iso_url_pattern_match(\"/\", \"/\")\n    expected = { 'params' => {} }\n    assert_equal expected, result\n  end\n\n  def test_base_route_no_match\n    # Base route - no match\n    result = preact_iso_url_pattern_match(\"/user/1\", \"/\")\n    assert_nil result\n  end\n\n  # Param route tests\n  def test_param_route_match\n    # Param route - match\n    result = preact_iso_url_pattern_match(\"/user/2\", \"/user/:id\")\n    expected = { 'params' => { 'id' => '2' }, 'id' => '2' }\n    assert_equal expected, result\n  end\n\n  def test_param_route_no_match\n    # Param route - no match\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id\")\n    assert_nil result\n  end\n\n  # Rest segment tests\n  def test_rest_segment_match\n    # Rest segment - match\n    result = preact_iso_url_pattern_match(\"/user/foo\", \"/user/*\")\n    expected = { 'params' => {}, 'rest' => '/foo' }\n    assert_equal expected, result\n  end\n\n  def test_rest_segment_match_multiple_segments\n    # Rest segment - match multiple segments\n    result = preact_iso_url_pattern_match(\"/user/foo/bar/baz\", \"/user/*\")\n    expected = { 'params' => {}, 'rest' => '/foo/bar/baz' }\n    assert_equal expected, result\n  end\n\n  def test_rest_segment_no_match\n    # Rest segment - no match\n    result = preact_iso_url_pattern_match(\"/user\", \"/user/*\")\n    assert_nil result\n  end\n\n  def test_rest_segment_no_match_different_case\n    # Rest segment - no match different case\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id/*\")\n    assert_nil result\n  end\n\n  # Param route with rest segment\n  def test_param_with_rest_single_segment\n    # Param with rest - single segment\n    result = preact_iso_url_pattern_match(\"/user/2/foo\", \"/user/:id/*\")\n    expected = { 'params' => { 'id' => '2' }, 'id' => '2', 'rest' => '/foo' }\n    assert_equal expected, result\n  end\n\n  def test_param_with_rest_multiple_segments\n    # Param with rest - multiple segments\n    result = preact_iso_url_pattern_match(\"/user/2/foo/bar/bob\", \"/user/:id/*\")\n    expected = { 'params' => { 'id' => '2' }, 'id' => '2', 'rest' => '/foo/bar/bob' }\n    assert_equal expected, result\n  end\n\n  def test_param_with_rest_no_match\n    # Param with rest - no match\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id/*\")\n    assert_nil result\n  end\n\n  # Optional param tests\n  def test_optional_param_empty\n    # Optional param - empty\n    result = preact_iso_url_pattern_match(\"/user\", \"/user/:id?\")\n    expected = { 'params' => { 'id' => nil }, 'id' => nil }\n    assert_equal expected, result\n  end\n\n  def test_optional_param_no_match_base\n    # Optional param - no match base\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id?\")\n    assert_nil result\n  end\n\n  # Optional rest param tests (/:x*)\n  def test_optional_rest_param_empty\n    # Optional rest param - empty\n    result = preact_iso_url_pattern_match(\"/user\", \"/user/:id*\")\n    expected = { 'params' => { 'id' => nil }, 'id' => nil }\n    assert_equal expected, result\n  end\n\n  def test_optional_rest_param_with_segments\n    # Optional rest param - with segments\n    result = preact_iso_url_pattern_match(\"/user/foo/bar\", \"/user/:id*\")\n    expected = { 'params' => { 'id' => 'foo/bar' }, 'id' => 'foo/bar' }\n    assert_equal expected, result\n  end\n\n  def test_optional_param_no_match_base_duplicate\n    # Optional param - no match base duplicate\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id*\")\n    assert_nil result\n  end\n\n  # Required rest param tests (/:x+)\n  def test_required_rest_param_single_segment\n    # Required rest param - single segment\n    result = preact_iso_url_pattern_match(\"/user/foo\", \"/user/:id+\")\n    expected = { 'params' => { 'id' => 'foo' }, 'id' => 'foo' }\n    assert_equal expected, result\n  end\n\n  def test_required_rest_param_multiple_segments\n    # Required rest param - multiple segments\n    result = preact_iso_url_pattern_match(\"/user/foo/bar\", \"/user/:id+\")\n    expected = { 'params' => { 'id' => 'foo/bar' }, 'id' => 'foo/bar' }\n    assert_equal expected, result\n  end\n\n  def test_required_rest_param_empty_should_fail\n    # Required rest param - empty (should fail)\n    result = preact_iso_url_pattern_match(\"/user\", \"/user/:id+\")\n    assert_nil result\n  end\n\n  def test_required_rest_param_root_mismatch\n    # Required rest param - root mismatch\n    result = preact_iso_url_pattern_match(\"/\", \"/user/:id+\")\n    assert_nil result\n  end\n\n  # Leading/trailing slashes\n  def test_leading_trailing_slashes\n    # Leading/trailing slashes\n    result = preact_iso_url_pattern_match(\"/about-late/_SEGMENT1_/_SEGMENT2_/\", \"/about-late/:seg1/:seg2/\")\n    expected = { 'params' => { 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_' }, 'seg1' => '_SEGMENT1_', 'seg2' => '_SEGMENT2_' }\n    assert_equal expected, result\n  end\n\n  # Additional tests that are not in test/node/router-match.test.js\n  # URL encoding tests\n  def test_url_encoded_param\n    # URL encoded param\n    result = preact_iso_url_pattern_match(\"/foo/bar%20baz\", \"/foo/:param\")\n    expected = { 'params' => { 'param' => 'bar baz' }, 'param' => 'bar baz' }\n    assert_equal expected, result\n  end\n\n  def test_url_encoded_email_in_param\n    # URL encoded email in param\n    result = preact_iso_url_pattern_match(\"/users/test%40example.com/posts\", \"/users/:userId/posts\")\n    expected = { 'params' => { 'userId' => 'test@example.com' }, 'userId' => 'test@example.com' }\n    assert_equal expected, result\n  end\n\n  # Complex rest segment with encoding\n  def test_rest_segment_with_encoded_parts\n    # Rest segment with encoded parts\n    result = preact_iso_url_pattern_match(\"/api/path/with%20spaces/and%2Fslashes\", \"/api/:path+\")\n    expected = { 'params' => { 'path' => 'path/with spaces/and/slashes' }, 'path' => 'path/with spaces/and/slashes' }\n    assert_equal expected, result\n  end\n\n  # Edge cases\n  def test_empty_route\n    # Empty route\n    result = preact_iso_url_pattern_match(\"/foo\", \"\")\n    assert_nil result\n  end\n\n  def test_empty_url_with_param\n    # Empty url with param\n    result = preact_iso_url_pattern_match(\"\", \"/:param\")\n    assert_nil result\n  end\n\n  def test_mixed_required_and_optional_params\n    # Mixed required and optional params\n    result = preact_iso_url_pattern_match(\"/foo/bar\", \"/:required/:optional?\")\n    expected = { 'params' => { 'required' => 'foo', 'optional' => 'bar' }, 'required' => 'foo', 'optional' => 'bar' }\n    assert_equal expected, result\n  end\n\n  def test_mixed_required_and_optional_params_missing_optional\n    # Mixed required and optional params - missing optional\n    result = preact_iso_url_pattern_match(\"/foo\", \"/:required/:optional?\")\n    expected = { 'params' => { 'required' => 'foo', 'optional' => nil }, 'required' => 'foo', 'optional' => nil }\n    assert_equal expected, result\n  end\n\n  # Test with pre-existing matches\n  def test_pre_existing_matches_object\n    # Pre-existing matches object\n    matches = { 'params' => { 'existing' => 'value' } }\n    result = preact_iso_url_pattern_match(\"/foo/bar\", \"/:first/:second\", matches)\n    expected = { 'params' => { 'existing' => 'value', 'first' => 'foo', 'second' => 'bar' }, 'first' => 'foo', 'second' => 'bar' }\n    assert_equal expected, result\n  end\n\n  # Complex nested paths\n  def test_complex_nested_path_with_multiple_params\n    # Complex nested path with multiple params\n    result = preact_iso_url_pattern_match(\"/api/v1/users/123/posts/456/comments\", \"/api/:version/users/:userId/posts/:postId/comments\")\n    expected = {\n      'params' => { 'version' => 'v1', 'userId' => '123', 'postId' => '456' },\n      'version' => 'v1', 'userId' => '123', 'postId' => '456'\n    }\n    assert_equal expected, result\n  end\n\n  def test_route_longer_than_url_required_param_missing\n    # Route longer than URL - required param missing\n    result = preact_iso_url_pattern_match(\"/api\", \"/api/:version/:resource\")\n    assert_nil result\n  end\n\n  def test_route_longer_than_url_optional_param\n    # Route longer than URL - optional param\n    result = preact_iso_url_pattern_match(\"/api\", \"/api/:version?\")\n    expected = { 'params' => { 'version' => nil }, 'version' => nil }\n    assert_equal expected, result\n  end\n\n\n  def test_multiple_slashes_in_url_should_be_normalized\n    # Multiple slashes in URL should be normalized\n    result = preact_iso_url_pattern_match(\"//user//123//\", \"/user/:id\")\n    expected = { 'params' => { 'id' => '123' }, 'id' => '123' }\n    assert_equal expected, result\n  end\n\n  def test_route_with_multiple_slashes\n    # Route with multiple slashes\n    result = preact_iso_url_pattern_match(\"/user/123\", \"//user//:id//\")\n    expected = { 'params' => { 'id' => '123' }, 'id' => '123' }\n    assert_equal expected, result\n  end\n\n  def test_rest_param_with_single_character\n    # Rest param with single character\n    result = preact_iso_url_pattern_match(\"/a/b\", \"/:x+\")\n    expected = { 'params' => { 'x' => 'a/b' }, 'x' => 'a/b' }\n    assert_equal expected, result\n  end\n\n  def test_complex_url_encoding_in_rest_params\n    # Complex URL encoding in rest params\n    result = preact_iso_url_pattern_match(\"/files/folder%2Fsubfolder/file%20name.txt\", \"/files/:path+\")\n    expected = { 'params' => { 'path' => 'folder/subfolder/file name.txt' }, 'path' => 'folder/subfolder/file name.txt' }\n    assert_equal expected, result\n  end\n\n  def test_special_characters_encoded_in_url\n    # Special characters encoded in URL\n    result = preact_iso_url_pattern_match(\"/search/query%3F%2B%23%26test\", \"/search/:query\")\n    expected = { 'params' => { 'query' => 'query?+#&test' }, 'query' => 'query?+#&test' }\n    assert_equal expected, result\n  end\n\n  def test_unicode_characters_encoded\n    # Unicode characters encoded\n    result = preact_iso_url_pattern_match(\"/user/Jos%C3%A9\", \"/user/:name\")\n    expected = { 'params' => { 'name' => 'José' }, 'name' => 'José' }\n    assert_equal expected, result\n  end\n\n  def test_empty_segments_in_middle_of_url\n    # Empty segments in middle of URL\n    result = preact_iso_url_pattern_match(\"/api//v1//users\", \"/api/v1/users\")\n    expected = { 'params' => {} }\n    assert_equal expected, result\n  end\n\n  def test_route_with_only_wildcards\n    # Route with only wildcards\n    result = preact_iso_url_pattern_match(\"/anything/goes/here\", \"*\")\n    expected = { 'params' => {}, 'rest' => '/anything/goes/here' }\n    assert_equal expected, result\n  end\n\nend\n\nclass TestUrlDecodingErrorHandling < Minitest::Test\n  # Tests specifically for URL decoding error scenarios\n\n  def test_malformed_percent_encoding_simple_param\n    # Test malformed percent encoding in simple param - should not crash\n    # This should handle malformed encoding gracefully\n    result = preact_iso_url_pattern_match(\"/user/test%\", \"/user/:id\")\n    # Should either work or return nil, but not crash\n    refute_nil result\n  end\n\n  def test_malformed_percent_encoding_rest_param\n    # Test malformed percent encoding in rest param - should not crash\n    result = preact_iso_url_pattern_match(\"/files/test%/file\", \"/files/:path+\")\n    # Should either work or return nil, but not crash\n    refute_nil result\n  end\n\n  def test_invalid_unicode_sequence\n    # Test invalid unicode sequence - should not crash\n    result = preact_iso_url_pattern_match(\"/user/test%C3\", \"/user/:id\")\n    # Should either work or return nil, but not crash\n    refute_nil result\n  end\nend\n\n# Run tests if this file is executed directly\nif __FILE__ == $0\n  puts \"Running Ruby tests for preact-iso-url-pattern...\"\nend\n"
  },
  {
    "path": "polyglot-utils/run_tests.sh",
    "content": "#!/bin/bash\n\n# Preact ISO URL Pattern Matching - Test Runner\n# Runs tests for all language implementations\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Track test results\nTOTAL_LANGUAGES=4\nPASSED_LANGUAGES=0\n\necho \"========================================\"\necho \"Preact ISO URL Pattern - Test Runner\"\necho \"========================================\"\necho\n\n# Function to run a test and track results\nrun_test() {\n    local language=$1\n    local directory=$2\n    local command=$3\n    local description=$4\n\n    echo -e \"${BLUE}Testing $language${NC} ($description)\"\n    echo \"----------------------------------------\"\n\n    # Change to test directory and run command\n    if cd \"$directory\" 2>/dev/null; then\n        if eval \"$command\"; then\n            echo -e \"${GREEN}$language tests PASSED${NC}\"\n            ((PASSED_LANGUAGES++))\n        else\n            echo -e \"${RED}$language tests FAILED${NC}\"\n        fi\n    else\n        echo -e \"${RED}$language tests FAILED - Directory not found${NC}\"\n    fi\n\n    echo\n    # Return to script directory\n    cd \"$SCRIPT_DIR\" 2>/dev/null || true\n}\n\n# Get the script directory to ensure we're in the right place\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\n# Run tests for each language\nrun_test \"Go\" \"go\" \"go test -v\" \"Static typing with struct returns\"\nrun_test \"Python\" \"python\" \"python3 test_preact_iso_url_pattern.py\" \"Dictionary-based with optional typing\"\nrun_test \"Ruby\" \"ruby\" \"ruby test_preact_iso_url_pattern.rb\" \"Hash-based with flexible syntax\"\nrun_test \"PHP\" \"php\" \"php test_preact_iso_url_pattern.php\" \"Mixed array/object approach\"\n\n# Summary\necho \"========================================\"\necho \"Test Summary\"\necho \"========================================\"\n\nif [ $PASSED_LANGUAGES -eq $TOTAL_LANGUAGES ]; then\n    echo -e \"${GREEN}All $TOTAL_LANGUAGES language implementations passed their tests!${NC}\"\n    echo -e \"${GREEN}Total tests across all languages: 204 (51 × 4)${NC}\"\n    exit 0\nelse\n    echo -e \"${RED}$PASSED_LANGUAGES/$TOTAL_LANGUAGES language implementations passed${NC}\"\n    echo -e \"${RED}$(($TOTAL_LANGUAGES - $PASSED_LANGUAGES)) language(s) failed${NC}\"\n    exit 1\nfi\n"
  },
  {
    "path": "src/hydrate.d.ts",
    "content": "import { ComponentChild, ContainerNode } from 'preact';\n\nexport default function hydrate(jsx: ComponentChild, parent?: ContainerNode): void;\n"
  },
  {
    "path": "src/hydrate.js",
    "content": "import { render, hydrate as hydrativeRender } from 'preact';\n\nlet initialized;\n\n/** @type {typeof hydrativeRender} */\nexport default function hydrate(jsx, parent) {\n\tif (typeof window === 'undefined') return;\n\tlet isodata = document.querySelector('script[type=isodata]');\n\t// @ts-ignore-next\n\tparent = parent || (isodata && isodata.parentNode) || document.body;\n\tif (!initialized && isodata) {\n\t\thydrativeRender(jsx, parent);\n\t} else {\n\t\trender(jsx, parent);\n\t}\n\tinitialized = true;\n}\n"
  },
  {
    "path": "src/index.d.ts",
    "content": "export { default as prerender } from './prerender.js';\nexport * from './router.js';\nexport { default as lazy, ErrorBoundary } from './lazy.js';\nexport { default as hydrate } from './hydrate.js';\n"
  },
  {
    "path": "src/index.js",
    "content": "export { Router, LocationProvider, useLocation, Route, useRoute } from './router.js';\nexport { default as lazy, ErrorBoundary } from './lazy.js';\nexport { default as hydrate } from './hydrate.js';\n\nexport function prerender(vnode, options) {\n\treturn import('./prerender.js').then(m => m.default(vnode, options));\n}\n"
  },
  {
    "path": "src/internal.d.ts",
    "content": "/// <reference types=\"navigation-api-types\" />\nimport { Component } from 'preact';\n\nexport interface AugmentedComponent extends Component<any, any> {\n\t__v: VNode;\n\t__c: (error: Promise<void>, suspendingVNode: VNode) => void;\n}\n\nexport interface VNode<P = any> extends preact.VNode<P> {\n\t__c: AugmentedComponent;\n\t__e?: Element | Text;\n\t__u: number;\n\t__h: boolean;\n\t__v?: VNode<P>;\n\t__k: Array<VNode<any>> | null;\n}\n\nexport {}\n"
  },
  {
    "path": "src/lazy.d.ts",
    "content": "import { ComponentChildren, VNode } from 'preact';\n\nexport default function lazy<T>(load: () => Promise<{ default: T } | T>): T & {\n\tpreload: () => Promise<T>;\n};\n\nexport function ErrorBoundary(props: { children?: ComponentChildren; onError?: (error: Error) => void }): VNode;\n"
  },
  {
    "path": "src/lazy.js",
    "content": "import { h, options } from 'preact';\nimport { useState, useRef } from 'preact/hooks';\n\nconst oldDiff = options.__b;\noptions.__b = (vnode) => {\n\tif (vnode.type && vnode.type._forwarded && vnode.ref) {\n\t\tvnode.props.ref = vnode.ref;\n\t\tvnode.ref = null;\n\t}\n\tif (oldDiff) oldDiff(vnode);\n};\n\nexport default function lazy(load) {\n\tlet p, c;\n\n\tconst loadModule = () =>\n\t\tload().then(m => (c = (m && m.default) || m));\n\n\tconst LazyComponent = props => {\n\t\tconst [, update] = useState(0);\n\t\tconst r = useRef(c);\n\t\tif (!p) p = loadModule();\n\t\tif (c !== undefined) return h(c, props);\n\t\tif (!r.current) r.current = p.then(() => update(1));\n\t\tthrow p;\n\t};\n\n\tLazyComponent.preload = () => {\n\t\tif (!p) p = loadModule();\n\t\treturn p;\n\t}\n\n\tLazyComponent._forwarded = true;\n\treturn LazyComponent;\n}\n\n// See https://github.com/preactjs/preact/blob/88680e91ec0d5fc29d38554a3e122b10824636b6/compat/src/suspense.js#L5\nconst oldCatchError = options.__e;\noptions.__e = (err, newVNode, oldVNode) => {\n\tif (err && err.then) {\n\t\tlet v = newVNode;\n\t\twhile ((v = v.__)) {\n\t\t\tif (v.__c && v.__c.__c) {\n\t\t\t\tif (newVNode.__e == null) {\n\t\t\t\t\tnewVNode.__c.__z = [oldVNode.__e];\n\t\t\t\t\tnewVNode.__e = oldVNode.__e; // ._dom\n\t\t\t\t\tnewVNode.__k = oldVNode.__k; // ._children\n\t\t\t\t}\n\t\t\t\tif (!newVNode.__k) newVNode.__k = [];\n\t\t\t\treturn v.__c.__c(err, newVNode);\n\t\t\t}\n\t\t}\n\t}\n\tif (oldCatchError) oldCatchError(err, newVNode, oldVNode);\n};\n\nexport function ErrorBoundary(props) {\n\tthis.__c = childDidSuspend;\n\tthis.componentDidCatch = props.onError;\n\treturn props.children;\n}\n\nfunction childDidSuspend(err) {\n\terr.then(() => this.forceUpdate());\n}\n"
  },
  {
    "path": "src/prerender.d.ts",
    "content": "import { VNode } from 'preact';\n\nexport interface PrerenderOptions {\n\tprops?: Record<string, unknown>;\n}\n\nexport interface PrerenderResult {\n\thtml: string;\n\tlinks?: Set<string>\n}\n\nexport default function prerender(\n\tvnode: VNode,\n\toptions?: PrerenderOptions\n): Promise<PrerenderResult>;\n\nexport function locationStub(path: string): void;\n"
  },
  {
    "path": "src/prerender.js",
    "content": "import { h, options, cloneElement } from 'preact';\nimport { renderToStringAsync } from 'preact-render-to-string';\n\nlet vnodeHook;\n\nconst old = options.vnode;\noptions.vnode = vnode => {\n\tif (old) old(vnode);\n\tif (vnodeHook) vnodeHook(vnode);\n};\n\n/**\n * @param {ReturnType<h>} vnode The root JSX element to render (eg: `<App />`)\n * @param {object} [options]\n * @param {object} [options.props] Additional props to merge into the root JSX element\n */\nexport default async function prerender(vnode, options) {\n\toptions = options || {};\n\n\tconst props = options.props;\n\n\tif (typeof vnode === 'function') {\n\t\tvnode = h(vnode, props);\n\t} else if (props) {\n\t\tvnode = cloneElement(vnode, props);\n\t}\n\n\tlet links = new Set();\n\tvnodeHook = ({ type, props }) => {\n\t\tif (type === 'a' && props && props.href && (!props.target || props.target === '_self')) {\n\t\t\tlinks.add(props.href);\n\t\t}\n\t};\n\n\ttry {\n\t\tlet html = await renderToStringAsync(vnode);\n\t\thtml += `<script type=\"isodata\"></script>`;\n\t\treturn { html, links };\n\t} finally {\n\t\tvnodeHook = null;\n\t}\n}\n\n/**\n * Update `location` to current URL so routers can use things like `location.pathname`\n *\n * @param {string} path - current URL path\n */\nexport function locationStub(path) {\n\tglobalThis.location = {};\n\tconst u = new URL(path, 'http://localhost');\n\tfor (const i in u) {\n\t\ttry {\n\t\t\tglobalThis.location[i] = /to[A-Z]/.test(i)\n\t\t\t\t? u[i].bind(u)\n\t\t\t\t: String(u[i]);\n\t\t} catch {}\n\t}\n}\n"
  },
  {
    "path": "src/router-navigation-api.d.ts",
    "content": "import { AnyComponent, ComponentChildren, Context, VNode } from 'preact';\n\nexport const LocationProvider: {\n\t(props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode;\n\tctx: Context<LocationHook>;\n};\n\ntype NestedArray<T> = Array<T | NestedArray<T>>;\n\ninterface KnownProps {\n\tpath: string;\n\tquery: Record<string, string>;\n\tparams: Record<string, string>;\n\tdefault?: boolean;\n\trest?: string;\n\tcomponent?: AnyComponent;\n}\n\ninterface ArbitraryProps {\n\t[prop: string]: any;\n}\n\ntype MatchProps = KnownProps & ArbitraryProps;\n\n/**\n * Check if a URL path matches against a URL path pattern.\n *\n * Warning: This is largely an internal API, it may change in the future\n * @param url - URL path (e.g. /user/12345)\n * @param route - URL pattern (e.g. /user/:id)\n */\nexport function exec(url: string, route: string, matches?: MatchProps): MatchProps\n\nexport function Router(props: {\n\tonRouteChange?: (url: string) => void;\n\tonLoadEnd?: (url: string) => void;\n\tonLoadStart?: (url: string) => void;\n\tchildren?: NestedArray<VNode>;\n}): VNode;\n\ninterface LocationHook {\n\turl: string;\n\tpath: string;\n\tquery: Record<string, string>;\n}\nexport const useLocation: () => LocationHook;\n\ninterface RouteHook {\n\tpath: string;\n\tquery: Record<string, string>;\n\tparams: Record<string, string>;\n}\nexport const useRoute: () => RouteHook;\n\ntype RoutableProps =\n\t| { path: string; default?: false; }\n\t| { path?: never; default: true; }\n\nexport type RouteProps<Props> = RoutableProps & { component: AnyComponent<Props> };\n\nexport type RoutePropsForPath<Path extends string> = Path extends '*'\n\t? { params: {}; rest: string }\n\n\t: Path extends `:${infer placeholder}?/${infer rest}`\n\t? { [k in placeholder]?: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]?: string } } & Omit<RoutePropsForPath<rest>, 'params'>\n\n\t: Path extends `:${infer placeholder}/${infer rest}`\n\t? { [k in placeholder]: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]: string } } & Omit<RoutePropsForPath<rest>, 'params'>\n\n\t: Path extends `:${infer placeholder}?`\n\t? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }\n\n\t: Path extends `:${infer placeholder}*`\n\t? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }\n\n\t: Path extends `:${infer placeholder}+`\n\t? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }\n\n\t: Path extends `:${infer placeholder}`\n\t? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }\n\n\t: Path extends (`/${infer rest}` | `${infer _}/${infer rest}`)\n\t? RoutePropsForPath<rest>\n\n\t: { params: {} };\n\nexport function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;\n\ndeclare module 'preact' {\n\t// The code below automatically adds `path` and `default` as optional props for every component\n\t// (effectively reserving those names, so no component should use those names in its own props).\n\t// These declarations extend from `RouteableProps`, which is not allowed in modern TypeScript and\n\t// causes a TS2312 error.  However, the compiler does seems to honor the intent of this code, so\n\t// to avoid an API regression, let's ignore the error rather than loosening the type validation.\n\tnamespace JSX {\n\t\t/** @ts-ignore */\n\t\tinterface IntrinsicAttributes extends RoutableProps {}\n\t}\n\t/** @ts-ignore */\n\tinterface Attributes extends RoutableProps {}\n}\n"
  },
  {
    "path": "src/router-navigation-api.js",
    "content": "import { h, createContext, cloneElement, toChildArray } from 'preact';\nimport { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks';\n\n/**\n * @template T\n * @typedef {import('preact').RefObject<T>} RefObject\n * @typedef {import('./internal.d.ts').VNode} VNode\n */\n\n/**\n * @param {NavigateEvent} e\n */\nfunction isSameWindow(e) {\n\tconst sourceElement = /** @type {HTMLAnchorElement | null} */ (e.sourceElement);\n\treturn (\n\t\t!sourceElement ||\n\t\t!sourceElement.target ||\n\t\t/^(_self)?$/i.test(sourceElement.target)\n\t);\n}\n\n/** @type {string | RegExp | undefined} */\nlet scope;\n\n/**\n * @param {URL} url\n * @returns {boolean}\n */\nfunction isInScope(url) {\n\treturn !scope || (typeof scope == 'string'\n\t\t? url.pathname.startsWith(scope)\n\t\t: scope.test(url.pathname)\n\t);\n}\n\n/**\n * @param {string} state\n * @param {NavigateEvent} e\n */\nfunction handleNav(state, e) {\n\tconst url = new URL(e.destination.url);\n\n\tif (\n\t\t!e.canIntercept ||\n\t\te.hashChange ||\n\t\te.downloadRequest !== null ||\n\t\t!isSameWindow(e) ||\n\t\t!isInScope(url)\n\t) {\n\t\t// This is set purely for our test suite so that we can check\n\t\t// if the event was ignored in another `navigate` handler.\n\t\te['preact-iso-ignored'] = true;\n\t\treturn state;\n\t}\n\n\te.intercept();\n\treturn url.href.replace(url.origin, '');\n}\n\nexport const exec = (url, route, matches = {}) => {\n\turl = url.split('/').filter(Boolean);\n\troute = (route || '').split('/').filter(Boolean);\n\tif (!matches.params) matches.params = {};\n\tfor (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {\n\t\tlet [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);\n\t\tval = url[i];\n\t\t// segment match:\n\t\tif (!m && param == val) continue;\n\t\t// /foo/* match\n\t\tif (!m && val && flag == '*') {\n\t\t\tmatches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/');\n\t\t\tbreak;\n\t\t}\n\t\t// segment mismatch / missing required field:\n\t\tif (!m || (!val && flag != '?' && flag != '*')) return;\n\t\trest = flag == '+' || flag == '*';\n\t\t// rest (+/*) match:\n\t\tif (rest) val = url.slice(i).join('/') || undefined;\n\t\t// normal/optional field:\n\t\telse if (val) val = decodeURIComponent(val);\n\t\tmatches.params[param] = val;\n\t\tif (!(param in matches)) matches[param] = val;\n\t\tif (rest) break;\n\t}\n\treturn matches;\n};\n\n/**\n * @param {Object} props\n * @param {string | RegExp} [props.scope]\n * @param {import('preact').ComponentChildren} [props.children]\n */\nexport function LocationProvider(props) {\n\tconst [url, route] = useReducer(handleNav, location.pathname + location.search);\n\tif (props.scope) scope = props.scope;\n\n\tconst value = useMemo(() => {\n\t\tconst u = new URL(url, location.origin);\n\t\tconst path = u.pathname.replace(/\\/+$/g, '') || '/';\n\t\treturn {\n\t\t\turl,\n\t\t\tpath,\n\t\t\tquery: Object.fromEntries(u.searchParams),\n\t\t};\n\t}, [url]);\n\n\tuseLayoutEffect(() => {\n\t\tnavigation.addEventListener('navigate', route)\n\n\t\treturn () => {\n\t\t\tnavigation.removeEventListener('navigate', route)\n\t\t};\n\t}, []);\n\n\treturn h(LocationProvider.ctx.Provider, { value }, props.children);\n}\n\nconst RESOLVED = Promise.resolve();\n/** @this {import('./internal.d.ts').AugmentedComponent} */\nexport function Router(props) {\n\tconst [c, update] = useReducer(c => c + 1, 0);\n\n\tconst { url, query, path } = useLocation();\n\tif (!url) {\n\t\tthrow new Error(`preact-iso's <Router> must be used within a <LocationProvider>, see: https://github.com/preactjs/preact-iso#locationprovider`);\n\t}\n\tconst { rest = path, params = {} } = useContext(RouteContext);\n\n\tconst isLoading = useRef(false);\n\tconst prevRoute = useRef(path);\n\t// Monotonic counter used to check if an un-suspending route is still the current route:\n\tconst count = useRef(0);\n\t// The current route:\n\tconst cur = /** @type {RefObject<VNode<any>>} */ (useRef());\n\t// Previous route (if current route is suspended):\n\tconst prev = /** @type {RefObject<VNode<any>>} */ (useRef());\n\t// A not-yet-hydrated DOM root to remove once we commit:\n\tconst pendingBase = /** @type {RefObject<Element | Text>} */ (useRef());\n\t// has this component ever successfully rendered without suspending:\n\tconst hasEverCommitted = useRef(false);\n\t// was the most recent render successful (did not suspend):\n\tconst didSuspend = /** @type {RefObject<boolean>} */ (useRef());\n\tdidSuspend.current = false;\n\n\tlet pathRoute, defaultRoute, matchProps;\n\ttoChildArray(props.children).some((/** @type {VNode<any>} */ vnode) => {\n\t\tconst matches = exec(\n\t\t\trest,\n\t\t\tvnode.props.path,\n\t\t\t(matchProps = {\n\t\t\t\t...vnode.props,\n\t\t\t\tpath: rest,\n\t\t\t\tquery,\n\t\t\t\tparams: Object.assign({}, params),\n\t\t\t\trest: ''\n\t\t\t})\n\t\t);\n\t\tif (matches) return (pathRoute = cloneElement(vnode, matchProps));\n\t\tif (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);\n\t});\n\n\t/** @type {VNode<any> | undefined} */\n\tlet incoming = pathRoute || defaultRoute;\n\n\tconst isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;\n\tconst isHydratingBool = cur.current && cur.current.__h;\n\tconst routeChanged = useMemo(() => {\n\t\tprev.current = cur.current;\n\n\t\tcur.current = /** @type {VNode<any>} */ (h(RouteContext.Provider, { value: matchProps }, incoming));\n\n\t\t// Only mark as an update if the route component changed.\n\t\tconst outgoing = prev.current && prev.current.props.children;\n\t\tif (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {\n\t\t\t// This hack prevents Preact from diffing when we swap `cur` to `prev`:\n\t\t\tif (this.__v && this.__v.__k) this.__v.__k.reverse();\n\t\t\tcount.current++;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}, [url, JSON.stringify(matchProps)]);\n\n\tif (isHydratingSuspense) {\n\t\tcur.current.__u |= MODE_HYDRATE;\n\t\tcur.current.__u |= MODE_SUSPENDED;\n\t} else if (isHydratingBool) {\n\t\tcur.current.__h = true;\n\t}\n\n\t// Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children.\n\tconst p = prev.current;\n\tprev.current = null;\n\n\t// This borrows the _childDidSuspend() solution from compat.\n\tthis.__c = (e, suspendedVNode) => {\n\t\t// Mark the current render as having suspended:\n\t\tdidSuspend.current = true;\n\n\t\t// The new route suspended, so keep the previous route around while it loads:\n\t\tprev.current = p;\n\n\t\t// Fire an event saying we're waiting for the route:\n\t\tif (props.onLoadStart) props.onLoadStart(url);\n\t\tisLoading.current = true;\n\n\t\t// Re-render on unsuspend:\n\t\tlet c = count.current;\n\t\te.then(() => {\n\t\t\t// Ignore this update if it isn't the most recently suspended update:\n\t\t\tif (c !== count.current) return;\n\n\t\t\t// Successful route transition: un-suspend after a tick and stop rendering the old route:\n\t\t\tprev.current = null;\n\t\t\tif (cur.current) {\n\t\t\t\tif (suspendedVNode.__h) {\n\t\t\t\t\t// _hydrating\n\t\t\t\t\tcur.current.__h = suspendedVNode.__h;\n\t\t\t\t}\n\n\t\t\t\tif (suspendedVNode.__u & MODE_SUSPENDED) {\n\t\t\t\t\t// _flags\n\t\t\t\t\tcur.current.__u |= MODE_SUSPENDED;\n\t\t\t\t}\n\n\t\t\t\tif (suspendedVNode.__u & MODE_HYDRATE) {\n\t\t\t\t\tcur.current.__u |= MODE_HYDRATE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tRESOLVED.then(update);\n\t\t});\n\t};\n\n\tuseLayoutEffect(() => {\n\t\tconst currentDom = this.__v && this.__v.__e;\n\n\t\t// Ignore suspended renders (failed commits):\n\t\tif (didSuspend.current) {\n\t\t\t// If we've never committed, mark any hydration DOM for removal on the next commit:\n\t\t\tif (!hasEverCommitted.current && !pendingBase.current) {\n\t\t\t\tpendingBase.current = currentDom;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// If this is the first ever successful commit and we didn't use the hydration DOM, remove it:\n\t\tif (!hasEverCommitted.current && pendingBase.current) {\n\t\t\tif (pendingBase.current !== currentDom) pendingBase.current.remove();\n\t\t\tpendingBase.current = null;\n\t\t}\n\n\t\t// Mark the component has having committed:\n\t\thasEverCommitted.current = true;\n\n\t\t// The route is loaded and rendered.\n\t\tif (prevRoute.current !== path) {\n\t\t\tif (props.onRouteChange) props.onRouteChange(url);\n\n\t\t\tprevRoute.current = path;\n\t\t}\n\n\t\tif (props.onLoadEnd && isLoading.current) props.onLoadEnd(url);\n\t\tisLoading.current = false;\n\t}, [path, c]);\n\n\t// Note: cur MUST render first in order to set didSuspend & prev.\n\treturn routeChanged\n\t\t? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })]\n\t\t: h(RenderRef, { r: cur });\n}\n\nconst MODE_HYDRATE = 1 << 5;\nconst MODE_SUSPENDED = 1 << 7;\n\n// Lazily render a ref's current value:\nconst RenderRef = ({ r }) => r.current;\n\nRouter.Provider = LocationProvider;\n\nLocationProvider.ctx = createContext(\n\t/** @type {import('./router-navigation-api.d.ts').LocationHook} */ ({})\n);\nconst RouteContext = createContext(\n\t/** @type {import('./router-navigation-api.d.ts').RouteHook & { rest: string }} */ ({})\n);\n\nexport const Route = props => h(props.component, props);\n\nexport const useLocation = () => useContext(LocationProvider.ctx);\nexport const useRoute = () => useContext(RouteContext);\n"
  },
  {
    "path": "src/router.d.ts",
    "content": "import { AnyComponent, ComponentChildren, Context, VNode } from 'preact';\n\nexport const LocationProvider: {\n\t(props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode;\n\tctx: Context<LocationHook>;\n};\n\ntype NestedArray<T> = Array<T | NestedArray<T>>;\n\ninterface KnownProps {\n\tpath: string;\n\tquery: Record<string, string>;\n\tparams: Record<string, string>;\n\tdefault?: boolean;\n\trest?: string;\n\tcomponent?: AnyComponent;\n}\n\ninterface ArbitraryProps {\n\t[prop: string]: any;\n}\n\ntype MatchProps = KnownProps & ArbitraryProps;\n\n/**\n * Check if a URL path matches against a URL path pattern.\n *\n * Warning: This is largely an internal API, it may change in the future\n * @param url - URL path (e.g. /user/12345)\n * @param route - URL pattern (e.g. /user/:id)\n */\nexport function exec(url: string, route: string, matches?: MatchProps): MatchProps\n\nexport function Router(props: {\n\tonRouteChange?: (url: string) => void;\n\tonLoadEnd?: (url: string) => void;\n\tonLoadStart?: (url: string) => void;\n\tchildren?: NestedArray<VNode>;\n}): VNode;\n\ninterface LocationHook {\n\turl: string;\n\tpath: string;\n\tquery: Record<string, string>;\n\troute: (url: string, replace?: boolean) => void;\n\tback: () => void;\n\tforward: () => void;\n}\nexport const useLocation: () => LocationHook;\n\ninterface RouteHook {\n\tpath: string;\n\tquery: Record<string, string>;\n\tparams: Record<string, string>;\n}\nexport const useRoute: () => RouteHook;\n\ntype RoutableProps =\n\t| { path: string; default?: false; }\n\t| { path?: never; default: true; }\n\nexport type RouteProps<Props> = RoutableProps & { component: AnyComponent<Props> };\n\nexport type RoutePropsForPath<Path extends string> = Path extends '*'\n\t? { params: {}; rest: string }\n\n\t: Path extends `:${infer placeholder}?/${infer rest}`\n\t? { [k in placeholder]?: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]?: string } } & Omit<RoutePropsForPath<rest>, 'params'>\n\n\t: Path extends `:${infer placeholder}/${infer rest}`\n\t? { [k in placeholder]: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]: string } } & Omit<RoutePropsForPath<rest>, 'params'>\n\n\t: Path extends `:${infer placeholder}?`\n\t? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }\n\n\t: Path extends `:${infer placeholder}*`\n\t? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }\n\n\t: Path extends `:${infer placeholder}+`\n\t? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }\n\n\t: Path extends `:${infer placeholder}`\n\t? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }\n\n\t: Path extends (`/${infer rest}` | `${infer _}/${infer rest}`)\n\t? RoutePropsForPath<rest>\n\n\t: { params: {} };\n\nexport function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;\n\ndeclare module 'preact' {\n\t// The code below automatically adds `path` and `default` as optional props for every component\n\t// (effectively reserving those names, so no component should use those names in its own props).\n\t// These declarations extend from `RouteableProps`, which is not allowed in modern TypeScript and\n\t// causes a TS2312 error.  However, the compiler does seems to honor the intent of this code, so\n\t// to avoid an API regression, let's ignore the error rather than loosening the type validation.\n\tnamespace JSX {\n\t\t/** @ts-ignore */\n\t\tinterface IntrinsicAttributes extends RoutableProps {}\n\t}\n\t/** @ts-ignore */\n\tinterface Attributes extends RoutableProps {}\n}\n"
  },
  {
    "path": "src/router.js",
    "content": "import { h, createContext, cloneElement, toChildArray } from 'preact';\nimport { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks';\n\n/**\n * @template T\n * @typedef {import('preact').RefObject<T>} RefObject\n * @typedef {import('./internal.d.ts').VNode} VNode\n */\n\n/** @type {boolean} */\nlet push;\n/** @type {string | RegExp | undefined} */\nlet scope;\n\n/**\n * @param {string} href\n * @returns {boolean}\n */\nfunction isInScope(href) {\n\treturn !scope || (typeof scope == 'string'\n\t\t? href.startsWith(scope)\n\t\t: scope.test(href)\n\t);\n}\n\n/**\n * @param {string} state\n * @param {MouseEvent | PopStateEvent | { url: string, replace?: boolean }} action\n */\nfunction handleNav(state, action) {\n\tlet url = '';\n\tpush = undefined;\n\tif (action && action.type === 'click') {\n\t\t// ignore events the browser takes care of already:\n\t\tif (action.ctrlKey || action.metaKey || action.altKey || action.shiftKey || action.button !== 0) {\n\t\t\treturn state;\n\t\t}\n\n\t\tconst link = action.composedPath().find(el => el.nodeName == 'A' && el.href),\n\t\t\thref = link && link.getAttribute('href');\n\t\tif (\n\t\t\t!link ||\n\t\t\tlink.origin != location.origin ||\n\t\t\t/^#/.test(href) ||\n\t\t\t!/^(_?self)?$/i.test(link.target) ||\n\t\t\t!isInScope(href) ||\n\t\t\tlink.download\n\t\t) {\n\t\t\treturn state;\n\t\t}\n\n\t\tpush = true;\n\t\taction.preventDefault();\n\t\turl = link.href.replace(location.origin, '');\n\t} else if (action && action.url) {\n\t\tpush = !action.replace;\n\t\turl = action.url;\n\t} else {\n\t\turl = location.pathname + location.search;\n\t}\n\n\tif (push === true) history.pushState(null, '', url);\n\telse if (push === false) history.replaceState(null, '', url);\n\n\treturn url;\n};\n\nexport const exec = (url, route, matches = {}) => {\n\turl = url.split('/').filter(Boolean);\n\troute = (route || '').split('/').filter(Boolean);\n\tif (!matches.params) matches.params = {};\n\tfor (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {\n\t\tlet [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);\n\t\tval = url[i];\n\t\t// segment match:\n\t\tif (!m && param == val) continue;\n\t\t// /foo/* match\n\t\tif (!m && val && flag == '*') {\n\t\t\tmatches.rest = '/' + url.slice(i).join('/');\n\t\t\tbreak;\n\t\t}\n\t\t// segment mismatch / missing required field:\n\t\tif (!m || (!val && flag != '?' && flag != '*')) return;\n\t\trest = flag == '+' || flag == '*';\n\t\t// rest (+/*) match:\n\t\tif (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined;\n\t\t// normal/optional field:\n\t\telse if (val) val = decodeURIComponent(val);\n\t\tmatches.params[param] = val;\n\t\tif (!(param in matches)) matches[param] = val;\n\t\tif (rest) break;\n\t}\n\treturn matches;\n};\n\n/**\n * @param {Object} props\n * @param {string | RegExp} [props.scope]\n * @param {import('preact').ComponentChildren} [props.children]\n */\nexport function LocationProvider(props) {\n\t// @ts-expect-error - props.url is not implemented correctly & will be removed in the future\n\tconst [url, route] = useReducer(handleNav, props.url || location.pathname + location.search);\n\tif (props.scope) scope = props.scope;\n\tconst wasPush = push === true;\n\n\tconst value = useMemo(() => {\n\t\tconst u = new URL(url, location.origin);\n\t\tconst path = u.pathname.replace(/\\/+$/g, '') || '/';\n\t\t// @ts-ignore-next\n\t\treturn {\n\t\t\turl,\n\t\t\tpath,\n\t\t\tquery: Object.fromEntries(u.searchParams),\n\t\t\troute: (url, replace) => route({ url, replace }),\n\t\t\tback: () => {\n\t\t\t\thistory.back();\n\t\t\t},\n\t\t\tforward: () => {\n\t\t\t\thistory.forward();\n\t\t\t},\n\t\t\twasPush\n\t\t};\n\t}, [url]);\n\n\tuseLayoutEffect(() => {\n\t\taddEventListener('click', route);\n\t\taddEventListener('popstate', route);\n\n\t\treturn () => {\n\t\t\tremoveEventListener('click', route);\n\t\t\tremoveEventListener('popstate', route);\n\t\t};\n\t}, []);\n\n\t// @ts-ignore\n\treturn h(LocationProvider.ctx.Provider, { value }, props.children);\n}\n\nconst RESOLVED = Promise.resolve();\n/** @this {import('./internal.d.ts').AugmentedComponent} */\nexport function Router(props) {\n\tconst [c, update] = useReducer(c => c + 1, 0);\n\n\tconst { url, query, wasPush, path } = useLocation();\n\tif (!url) {\n\t\tthrow new Error(`preact-iso's <Router> must be used within a <LocationProvider>, see: https://github.com/preactjs/preact-iso#locationprovider`);\n\t}\n\tconst { rest = path, params = {} } = useContext(RouteContext);\n\n\tconst isLoading = useRef(false);\n\tconst prevRoute = useRef(path);\n\t// Monotonic counter used to check if an un-suspending route is still the current route:\n\tconst count = useRef(0);\n\t// The current route:\n\tconst cur = /** @type {RefObject<VNode<any>>} */ (useRef());\n\t// Previous route (if current route is suspended):\n\tconst prev = /** @type {RefObject<VNode<any>>} */ (useRef());\n\t// A not-yet-hydrated DOM root to remove once we commit:\n\tconst pendingBase = /** @type {RefObject<Element | Text>} */ (useRef());\n\t// has this component ever successfully rendered without suspending:\n\tconst hasEverCommitted = useRef(false);\n\t// was the most recent render successful (did not suspend):\n\tconst didSuspend = /** @type {RefObject<boolean>} */ (useRef());\n\tdidSuspend.current = false;\n\n\tlet pathRoute, defaultRoute, matchProps;\n\ttoChildArray(props.children).some((/** @type {VNode<any>} */ vnode) => {\n\t\tconst matches = exec(\n\t\t\trest,\n\t\t\tvnode.props.path,\n\t\t\t(matchProps = {\n\t\t\t\t...vnode.props,\n\t\t\t\tpath: rest,\n\t\t\t\tquery,\n\t\t\t\tparams: Object.assign({}, params),\n\t\t\t\trest: ''\n\t\t\t})\n\t\t);\n\t\tif (matches) return (pathRoute = cloneElement(vnode, matchProps));\n\t\tif (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);\n\t});\n\n\t/** @type {VNode<any> | undefined} */\n\tlet incoming = pathRoute || defaultRoute;\n\n\tconst isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;\n\tconst isHydratingBool = cur.current && cur.current.__h;\n\tconst routeChanged = useMemo(() => {\n\t\tprev.current = cur.current;\n\n\t\tcur.current = /** @type {VNode<any>} */ (h(RouteContext.Provider, { value: matchProps }, incoming));\n\n\t\t// Only mark as an update if the route component changed.\n\t\tconst outgoing = prev.current && prev.current.props.children;\n\t\tif (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {\n\t\t\t// This hack prevents Preact from diffing when we swap `cur` to `prev`:\n\t\t\tif (this.__v && this.__v.__k) this.__v.__k.reverse();\n\t\t\tcount.current++;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}, [url, JSON.stringify(matchProps)]);\n\n\tif (isHydratingSuspense) {\n\t\tcur.current.__u |= MODE_HYDRATE;\n\t\tcur.current.__u |= MODE_SUSPENDED;\n\t} else if (isHydratingBool) {\n\t\tcur.current.__h = true;\n\t}\n\n\t// Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children.\n\tconst p = prev.current;\n\tprev.current = null;\n\n\t// This borrows the _childDidSuspend() solution from compat.\n\tthis.__c = (e, suspendedVNode) => {\n\t\t// Mark the current render as having suspended:\n\t\tdidSuspend.current = true;\n\n\t\t// The new route suspended, so keep the previous route around while it loads:\n\t\tprev.current = p;\n\n\t\t// Fire an event saying we're waiting for the route:\n\t\tif (props.onLoadStart) props.onLoadStart(url);\n\t\tisLoading.current = true;\n\n\t\t// Re-render on unsuspend:\n\t\tlet c = count.current;\n\t\te.then(() => {\n\t\t\t// Ignore this update if it isn't the most recently suspended update:\n\t\t\tif (c !== count.current) return;\n\n\t\t\t// Successful route transition: un-suspend after a tick and stop rendering the old route:\n\t\t\tprev.current = null;\n\t\t\tif (cur.current) {\n\t\t\t\tif (suspendedVNode.__h) {\n\t\t\t\t\t// _hydrating\n\t\t\t\t\tcur.current.__h = suspendedVNode.__h;\n\t\t\t\t}\n\n\t\t\t\tif (suspendedVNode.__u & MODE_SUSPENDED) {\n\t\t\t\t\t// _flags\n\t\t\t\t\tcur.current.__u |= MODE_SUSPENDED;\n\t\t\t\t}\n\n\t\t\t\tif (suspendedVNode.__u & MODE_HYDRATE) {\n\t\t\t\t\tcur.current.__u |= MODE_HYDRATE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tRESOLVED.then(update);\n\t\t});\n\t};\n\n\tuseLayoutEffect(() => {\n\t\tconst currentDom = this.__v && this.__v.__e;\n\n\t\t// Ignore suspended renders (failed commits):\n\t\tif (didSuspend.current) {\n\t\t\t// If we've never committed, mark any hydration DOM for removal on the next commit:\n\t\t\tif (!hasEverCommitted.current && !pendingBase.current) {\n\t\t\t\tpendingBase.current = currentDom;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// If this is the first ever successful commit and we didn't use the hydration DOM, remove it:\n\t\tif (!hasEverCommitted.current && pendingBase.current) {\n\t\t\tif (pendingBase.current !== currentDom) pendingBase.current.remove();\n\t\t\tpendingBase.current = null;\n\t\t}\n\n\t\t// Mark the component has having committed:\n\t\thasEverCommitted.current = true;\n\n\t\t// The route is loaded and rendered.\n\t\tif (prevRoute.current !== path) {\n\t\t\tif (wasPush) scrollTo(0, 0);\n\t\t\tif (props.onRouteChange) props.onRouteChange(url);\n\n\t\t\tprevRoute.current = path;\n\t\t}\n\n\t\tif (props.onLoadEnd && isLoading.current) props.onLoadEnd(url);\n\t\tisLoading.current = false;\n\t}, [path, wasPush, c]);\n\n\t// Note: cur MUST render first in order to set didSuspend & prev.\n\treturn routeChanged\n\t\t? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })]\n\t\t: h(RenderRef, { r: cur });\n}\n\nconst MODE_HYDRATE = 1 << 5;\nconst MODE_SUSPENDED = 1 << 7;\n\n// Lazily render a ref's current value:\nconst RenderRef = ({ r }) => r.current;\n\nRouter.Provider = LocationProvider;\n\nLocationProvider.ctx = createContext(\n\t/** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({})\n);\nconst RouteContext = createContext(\n\t/** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({})\n);\n\nexport const Route = props => h(props.component, props);\n\nexport const useLocation = () => useContext(LocationProvider.ctx);\nexport const useRoute = () => useContext(RouteContext);\n"
  },
  {
    "path": "test/lazy.test.js",
    "content": "import { h, render } from 'preact';\nimport * as chai from 'chai';\nimport * as sinon from 'sinon';\nimport sinonChai from 'sinon-chai';\n\nimport { LocationProvider, Router } from '../src/router.js';\nimport lazy, { ErrorBoundary } from '../src/lazy.js';\n\nimport './setup.js';\n\nconst expect = chai.expect;\nchai.use(sinonChai);\n\ndescribe('lazy', () => {\n\tlet scratch;\n\n\tbeforeEach(() => {\n\t\tif (scratch) {\n\t\t\trender(null, scratch);\n\t\t\tscratch.remove();\n\t\t}\n\t\tscratch = document.createElement('scratch');\n\t\tdocument.body.appendChild(scratch);\n\t\thistory.replaceState(null, null, '/');\n\t});\n\n\n\tit('should support preloading lazy imports', async () => {\n\t\tconst A = () => <h1>A</h1>;\n\t\tconst loadB = sinon.fake(() => Promise.resolve(() => <h1>B</h1>));\n\t\tconst B = lazy(loadB);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t<B path=\"/b\" />\n\t\t\t\t</Router>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(loadB).not.to.have.been.called;\n\t\tawait B.preload();\n\t\texpect(loadB).to.have.been.calledOnce;\n\t});\n\n\tit('should forward refs', async () => {\n\t\tconst A = (props) => <h1 ref={props.ref}>A</h1>;\n\t\tconst LazyA = lazy(() => Promise.resolve(A));\n\n\t\tconst ref = {};\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LazyA ref={ref} />\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\t\tawait new Promise(r => setTimeout(r, 1))\n\n\t\tif (ref.current.constructor === A) {\n\t\t\t// v10\n\t\t\texpect(ref.current.constructor).to.equal(A);\n\t\t} else {\n\t\t\t// v11+\n\t\t\texpect(ref.current).to.equal(scratch.firstChild);\n\t\t}\n\t});\n});\n\n"
  },
  {
    "path": "test/node/location-stub.test.js",
    "content": "import { test } from 'uvu';\nimport * as assert from 'uvu/assert';\n\nimport { locationStub } from '../../src/prerender.js';\n\ntest.before.each(() => {\n\tif (globalThis.location) {\n\t\tdelete globalThis.location;\n\t}\n});\n\ntest('Contains all Location instance properties', () => {\n\tlocationStub('/foo/bar?baz=qux#quux');\n\n\t[\n\t\t// 'ancestorOrigins', // Not supported by FireFox and sees little use, but we could add an empty val if it's needed\n\t\t'hash',\n\t\t'host',\n\t\t'hostname',\n\t\t'href',\n\t\t'origin',\n\t\t'pathname',\n\t\t'port',\n\t\t'protocol',\n\t\t'search',\n\t].forEach(key => {\n\t\tassert.ok(Object.hasOwn(globalThis.location, key), `missing: ${key}`);\n\t});\n});\n\n// Do we need to support `assign`, `reload`, and/or `replace`?\ntest('Support bound methods', () => {\n\tlocationStub('/foo/bar?baz=qux#quux');\n\n\tassert.equal(globalThis.location.toString(), 'http://localhost/foo/bar?baz=qux#quux')\n});\n\ntest.run();\n"
  },
  {
    "path": "test/node/pattern-match.types.ts",
    "content": "// Test this file by running:\n// npx tsc --noEmit test/node/pattern-match.types.ts\n\nimport type { RoutePropsForPath } from '../../src/router.js';\n\n// Test utils\n\ntype isEqualsType<T, U> = T extends U ? U extends T ? true : false : false;\ntype isWeakEqualsType<T, U> = T extends U ? true : false;\n\n// Type tests based on router-match.test.js cases\n\n// Base route test\nconst test1: isEqualsType<\n  RoutePropsForPath<'/'> ,\n\t{ params: {} }\n> = true;\n\nconst test1_1: isEqualsType<\n  RoutePropsForPath<'/'> ,\n\t{ arbitrary: {} }\n> = false;\n\n// Param route test\nconst test2: isEqualsType<\n  RoutePropsForPath<'/user/:id'> ,\n  { params: { id: string }, id: string }\n> = true;\n\nconst test2_weak: isWeakEqualsType<\n  RoutePropsForPath<'/user/:id'> ,\n  { params: { id: string } }\n> = true;\n\n// Param rest segment test\nconst test3: isEqualsType<\n  RoutePropsForPath<'/user/*'> ,\n  { params: {}, rest: string }\n> = true;\n\nconst test3_1: isEqualsType<\n  RoutePropsForPath<'/*'> ,\n  { params: {}, rest: string }\n> = true;\n\nconst test3_2: isEqualsType<\n  RoutePropsForPath<'*'> ,\n  { params: {}, rest: string }\n> = true;\n\n// Param route with rest segment test\nconst test4: isEqualsType<\n  RoutePropsForPath<'/user/:id/*'> ,\n  { params: { id: string }, id: string, rest: string }\n> = true;\n\n// Optional param route test\nconst test5: isEqualsType<\n  RoutePropsForPath<'/user/:id?'> ,\n\t{ params: { id?: string }, id?: string }\n> = true;\n\n// Optional rest param route \"/:x*\" test\nconst test6: isEqualsType<\n  RoutePropsForPath<'/user/:id*'> ,\n  { params: { id?: string }, id?: string }\n> = true;\n\n// rest param should not be present\nconst test6_error: isEqualsType<\n  RoutePropsForPath<'/user/:id*'> ,\n  { params: { id: string }, rest: string }\n> = false;\n\n// Rest param route \"/:x+\" test\nconst test7: isEqualsType<\n  RoutePropsForPath<'/user/:id+'> ,\n  { params: { id: string }, id: string }\n> = true;\n\n// rest param should not be present\nconst test7_error: isEqualsType<\n  RoutePropsForPath<'/user/:id+'>,\n  { params: { id: string }, id: string, rest: string }\n> = false;\n\n// Handles leading/trailing slashes test\nconst test8: isEqualsType<\n  RoutePropsForPath<'/about-late/:seg1/:seg2/'> ,\n  { params: { seg1: string; seg2: string }, seg1: string, seg2: string }\n> = true;\n\n// Multiple params test (from overwrite properties test)\nconst test9: isEqualsType<\n  RoutePropsForPath<'/:path/:query'> ,\n  { params: { path: string; query: string }, path: string, query: string }\n> = true;\n\n// Empty route test\nconst test10: isEqualsType<\n  RoutePropsForPath<''> ,\n  { params: {} }\n> = true;\n"
  },
  {
    "path": "test/node/prerender.test.js",
    "content": "import { test } from 'uvu';\nimport * as assert from 'uvu/assert';\nimport { html } from 'htm/preact';\n\nimport { default as prerender } from '../../src/prerender.js';\n\ntest('extracts links', async () => {\n\tconst App = () => html`\n\t\t<div>\n\t\t\t<a href=\"/foo\">foo</a>\n\t\t\t<a href=\"/bar\" target=\"_blank\">bar</a>\n\t\t\t<a href=\"/baz\" target=\"_self\">baz</a>\n\t\t</div>\n\t`;\n\n\n\tconst { links } = await prerender(html`<${App} />`);\n\tassert.equal(links.size, 2, `incorrect number of links found: ${links.size}`);\n\tassert.ok(links.has('/foo'), `missing: /foo`);\n\tassert.ok(links.has('/baz'), `missing: /baz`);\n});\n\ntest('appends iso data script', async () => {\n\tconst { html: h } = await prerender(html`<div />`);\n\t// Empty for now, but used for hydration vs render detection\n\tassert.match(h, /<script type=\"isodata\"><\\/script>/, 'missing iso data script tag');\n});\n\ntest.run();\n"
  },
  {
    "path": "test/node/router-match.test.js",
    "content": "import { test } from 'uvu';\nimport * as assert from 'uvu/assert';\n\nimport { exec } from '../../src/router.js';\n\nfunction execPath(path, pattern, opts) {\n\treturn exec(path, pattern, { path, query: {}, params: {}, ...(opts || {}) });\n}\n\ntest('Base route', () => {\n\tconst accurateResult = execPath('/', '/');\n\tassert.equal(accurateResult, { path: '/', params: {}, query: {} });\n\n\tconst inaccurateResult = execPath('/user/1', '/');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Param route', () => {\n\tconst accurateResult = execPath('/user/2', '/user/:id');\n\tassert.equal(accurateResult, { path: '/user/2', params: { id: '2' }, id: '2', query: {} });\n\n\tconst inaccurateResult = execPath('/', '/user/:id');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Param rest segment', () => {\n\tconst accurateResult = execPath('/user/foo', '/user/*');\n\tassert.equal(accurateResult, { path: '/user/foo', params: {}, query: {}, rest: '/foo' });\n\n\tconst accurateResult2 = execPath('/user/foo/bar/baz', '/user/*');\n\tassert.equal(accurateResult2, { path: '/user/foo/bar/baz', params: {}, query: {}, rest: '/foo/bar/baz' });\n\n\tconst inaccurateResult = execPath('/user', '/user/*');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Param route with rest segment', () => {\n\tconst accurateResult = execPath('/user/2/foo', '/user/:id/*');\n\tassert.equal(accurateResult, { path: '/user/2/foo', params: { id: '2' }, id: '2', query: {}, rest: '/foo' });\n\n\tconst accurateResult2 = execPath('/user/2/foo/bar/bob', '/user/:id/*');\n\tassert.equal(accurateResult2, {\n\t\tpath: '/user/2/foo/bar/bob',\n\t\tparams: { id: '2' },\n\t\tid: '2',\n\t\tquery: {},\n\t\trest: '/foo/bar/bob'\n\t});\n\n\tconst inaccurateResult = execPath('/', '/user/:id/*');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Optional param route', () => {\n\tconst accurateResult = execPath('/user', '/user/:id?');\n\tassert.equal(accurateResult, { path: '/user', params: { id: undefined }, id: undefined, query: {} });\n\n\tconst inaccurateResult = execPath('/', '/user/:id?');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Optional rest param route \"/:x*\"', () => {\n\tconst matchedResult = execPath('/user/foo', '/user/:id*');\n\tassert.equal(matchedResult, { path: '/user/foo', params: { id: 'foo' }, id: 'foo', query: {} });\n\n\tconst matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id*');\n\tassert.equal(matchedResultWithSlash, {\n\t\tpath: '/user/foo/bar',\n\t\tparams: { id: 'foo/bar' },\n\t\tid: 'foo/bar',\n\t\tquery: {}\n\t});\n\n\tconst emptyResult = execPath('/user', '/user/:id*');\n\tassert.equal(emptyResult, {\n\t\tpath: '/user',\n\t\tparams: { id: undefined },\n\t\tid: undefined,\n\t\tquery: {}\n\t});\n\n\tconst inaccurateResult = execPath('/', '/user/:id*');\n\tassert.equal(inaccurateResult, undefined);\n});\n\ntest('Rest param route \"/:x+\"', () => {\n\tconst matchedResult = execPath('/user/foo', '/user/:id+');\n\tassert.equal(matchedResult, { path: '/user/foo', params: { id: 'foo' }, id: 'foo', query: {} });\n\n\tconst matchedResultWithSlash = execPath('/user/foo/bar', '/user/:id+');\n\tassert.equal(matchedResultWithSlash, {\n\t\tpath: '/user/foo/bar',\n\t\tparams: { id: 'foo/bar' },\n\t\tid: 'foo/bar',\n\t\tquery: {}\n\t});\n\n\tconst emptyResult = execPath('/user', '/user/:id+');\n\tassert.equal(emptyResult, undefined);\n\n\tconst mismatchedResult = execPath('/', '/user/:id+');\n\tassert.equal(mismatchedResult, undefined);\n});\n\ntest('Handles leading/trailing slashes', () => {\n\tconst result = execPath('/about-late/_SEGMENT1_/_SEGMENT2_/', '/about-late/:seg1/:seg2/');\n\tassert.equal(result, {\n\t\tpath: '/about-late/_SEGMENT1_/_SEGMENT2_/',\n\t\tparams: {\n\t\t\tseg1: '_SEGMENT1_',\n\t\t\tseg2: '_SEGMENT2_'\n\t\t},\n\t\tseg1: '_SEGMENT1_',\n\t\tseg2: '_SEGMENT2_',\n\t\tquery: {}\n\t});\n});\n\ntest('Percent-encoded characters in rest are not decoded', () => {\n\tconst result = execPath('/nested/child/%25', '/nested/*');\n\tassert.equal(result, { path: '/nested/child/%25', params: {}, query: {}, rest: '/child/%25' });\n});\n\ntest('should not overwrite existing properties', () => {\n\tconst result = execPath('/foo/bar', '/:path/:query', { path: '/custom-path' });\n\tassert.equal(result, {\n\t\tparams: { path: 'foo', query: 'bar' },\n\t\tpath: '/custom-path',\n\t\tquery: {}\n\t});\n});\n\ntest.run();\n"
  },
  {
    "path": "test/router-navigation-api.test.js",
    "content": "import { h, Fragment, render, Component, hydrate, options } from 'preact';\nimport { useState } from 'preact/hooks';\nimport * as chai from 'chai';\nimport * as sinon from 'sinon';\nimport sinonChai from 'sinon-chai';\n\nimport { LocationProvider, Router, useLocation, Route, useRoute } from '../src/router-navigation-api.js';\nimport lazy, { ErrorBoundary } from '../src/lazy.js';\n\nimport './setup.js';\n\nconst expect = chai.expect;\nchai.use(sinonChai);\n\n/**\n * Usage:\n * - `await sleep(1)` for nav + loc/pushState/sync component check\n * - `await sleep(10)` for nav + async component check\n */\nconst sleep = ms => new Promise(r => setTimeout(r, ms));\n\n// delayed lazy()\nconst groggy = (component, ms) => lazy(() => sleep(ms).then(() => component));\n\ndescribe('Router', () => {\n\tlet scratch, loc;\n\n\tconst ShallowLocation = () => {\n\t\tloc = useLocation();\n\t\treturn null;\n\t}\n\n\tbeforeEach(() => {\n\t\tif (scratch) {\n\t\t\trender(null, scratch);\n\t\t\tscratch.remove();\n\t\t}\n\t\tloc = undefined;\n\t\tscratch = document.createElement('scratch');\n\t\tdocument.body.appendChild(scratch);\n\t\thistory.replaceState(null, null, '/');\n\t});\n\n\n\tit('should throw a clear error if the LocationProvider is missing', () => {\n\t\tconst Home = () => <h1>Home</h1>;\n\n\t\ttry {\n\t\t\trender(\n\t\t\t\t<Router>\n\t\t\t\t\t<Home path=\"/\" test=\"2\" />\n\t\t\t\t</Router>,\n\t\t\t\tscratch\n\t\t\t);\n\t\t\texpect.fail('should have thrown');\n\t\t} catch (e) {\n\t\t\texpect(e.message).to.include('must be used within a <LocationProvider>');\n\t\t}\n\t});\n\n\tit('should strip trailing slashes from path', async () => {\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tnavigation.navigate('/a/');\n\t\tawait sleep(1);\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/a/',\n\t\t\tpath: '/a',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should support class components using LocationProvider.ctx', () => {\n\t\tclass Foo extends Component {\n\t\t\tstatic contextType = LocationProvider.ctx;\n\n\t\t\trender() {\n\t\t\t\tloc = this.context;\n\t\t\t\treturn <h1>{loc.url}</h1>;\n\t\t\t}\n\t\t}\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Foo />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>/</h1>');\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should allow passing props to a route', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Home path=\"/\" test=\"2\" />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '2' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should allow updating props in a route', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\n\t\t/** @type {(string) => void} */\n\t\tlet set;\n\n\t\tconst App = () => {\n\t\t\tconst [test, setTest] = useState('2');\n\t\t\tset = setTest;\n\t\t\treturn (\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<Home path=\"/\" test={test} />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t);\n\t\t}\n\t\trender(<App />, scratch);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '2' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\n\t\tset('3')\n\t\tawait sleep(1);\n\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '3' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t});\n\n\tit('should switch between synchronous routes', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\t\tconst Profiles = sinon.fake(() => <h1>Profiles</h1>);\n\t\tconst Profile = sinon.fake(({ params }) => <h1>Profile: {params.id}</h1>);\n\t\tconst Fallback = sinon.fake(() => <h1>Fallback</h1>);\n\t\tconst stack = [];\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router onRouteChange={url => stack.push(url)}>\n\t\t\t\t\t<Home path=\"/\" />\n\t\t\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t\t\t<Profile path=\"/profiles/:id\" />\n\t\t\t\t\t<Fallback default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).not.to.have.been.called;\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\n\t\tHome.resetHistory();\n\t\tnavigation.navigate('/profiles');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Profiles');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).to.have.been.calledWith({ path: '/profiles', query: {}, params: {}, rest: '' });\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).not.to.have.been.called;\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/profiles',\n\t\t\tpath: '/profiles',\n\t\t\tquery: {}\n\t\t});\n\n\t\tProfiles.resetHistory();\n\t\tnavigation.navigate('/profiles/bob');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Profile: bob');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).to.have.been.calledWith(\n\t\t\t{ path: '/profiles/bob', query: {}, params: { id: 'bob' }, id: 'bob', rest: '' },\n\t\t);\n\t\texpect(Fallback).not.to.have.been.called;\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/profiles/bob',\n\t\t\tpath: '/profiles/bob',\n\t\t\tquery: {}\n\t\t});\n\n\t\tProfile.resetHistory();\n\t\tnavigation.navigate('/other?a=b&c=d');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Fallback');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).to.have.been.calledWith(\n\t\t\t{ default: true, path: '/other', query: { a: 'b', c: 'd' }, params: {}, rest: '' },\n\t\t);\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/other?a=b&c=d',\n\t\t\tpath: '/other',\n\t\t\tquery: { a: 'b', c: 'd' }\n\t\t});\n\t\texpect(stack).to.eql(['/profiles', '/profiles/bob', '/other?a=b&c=d']);\n\t});\n\n\tit('should wait for asynchronous routes', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst B = sinon.fake(groggy(() => route('B'), 1));\n\t\tconst C = sinon.fake(groggy(() => <h1>C</h1>, 1));\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b\" />\n\t\t\t\t\t\t<C path=\"/c\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tnavigation.navigate('/b');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).not.to.have.been.called;\n\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\t// We should never re-invoke <A /> while loading <B /> (that would be a remount of the old route):\n\t\texpect(A).not.to.have.been.called;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tB.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(B).to.have.been.calledOnce;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tB.resetHistory();\n\t\tnavigation.navigate('/c');\n\t\tnavigation.navigate('/c?1');\n\t\tnavigation.navigate('/c');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(B).not.to.have.been.called;\n\n\t\tawait sleep(1);\n\n\t\tnavigation.navigate('/c');\n\t\tnavigation.navigate('/c?2');\n\t\tnavigation.navigate('/c');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\t// We should never re-invoke <B /> while loading <C /> (that would be a remount of the old route):\n\t\texpect(B).not.to.have.been.called;\n\t\texpect(C).to.have.been.calledWith({ path: '/c', query: {}, params: {}, rest: '' });\n\n\t\tC.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>C</h1>');\n\t\texpect(C).to.have.been.calledOnce;\n\t\texpect(C).to.have.been.calledWith({ path: '/c', query: {}, params: {}, rest: '' });\n\n\t\t// \"instant\" routing to already-loaded routes\n\n\t\tC.resetHistory();\n\t\tB.resetHistory();\n\t\tnavigation.navigate('/b');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(C).not.to.have.been.called;\n\t\texpect(B).to.have.been.calledOnce;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tB.resetHistory();\n\t\tnavigation.navigate('/');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(B).not.to.have.been.called;\n\t\texpect(A).to.have.been.calledOnce;\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t});\n\n\tit('rerenders same-component routes rather than swap', async () => {\n\t\tconst A = sinon.fake(() => <h1>a</h1>);\n\t\tconst B = sinon.fake(groggy(({ sub }) => <h1>b/{sub}</h1>, 1));\n\n\t\t// Counts the wrappers around route components to determine what the Router is returning\n\t\t// Count will be 2 for switching route components, and 2 more if the new route is lazily loaded\n\t\t// A same-route navigation adds 1\n\t\tlet renderRefCount = 0;\n\n\t\tconst old = options.vnode;\n\t\toptions.vnode = (vnode) => {\n\t\t\tif (typeof vnode.type === 'function' && vnode.props.r !== undefined) {\n\t\t\t\trenderRefCount += 1;\n\t\t\t}\n\n\t\t\tif (old) old(vnode);\n\t\t}\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b/:sub\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>a</h1>');\n\t\texpect(renderRefCount).to.equal(2);\n\n\t\trenderRefCount = 0;\n\t\tnavigation.navigate('/b/a');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>b/a</h1>');\n\t\texpect(renderRefCount).to.equal(4);\n\n\t\trenderRefCount = 0;\n\t\tnavigation.navigate('/b/b');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>b/b</h1>');\n\t\texpect(renderRefCount).to.equal(1);\n\n\t\trenderRefCount = 0;\n\t\tnavigation.navigate('/');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>a</h1>');\n\t\texpect(renderRefCount).to.equal(2);\n\n\t\toptions.vnode = old;\n\t});\n\n\tit('should support onLoadStart/onLoadEnd/onRouteChange w/out navigation', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst loadStart = sinon.fake();\n\t\tconst loadEnd = sinon.fake();\n\t\tconst routeChange = sinon.fake();\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router\n\t\t\t\t\t\tonLoadStart={loadStart}\n\t\t\t\t\t\tonLoadEnd={loadEnd}\n\t\t\t\t\t\tonRouteChange={routeChange}\n\t\t\t\t\t>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t</Router>\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(loadStart).to.have.been.calledWith('/');\n\t\texpect(loadEnd).not.to.have.been.called;\n\t\texpect(routeChange).not.to.have.been.called;\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(loadStart).not.to.have.been.called;\n\t\texpect(loadEnd).to.have.been.calledWith('/');\n\t\texpect(routeChange).not.to.have.been.called;\n\t});\n\n\tit('should support onLoadStart/onLoadEnd/onRouteChange w/ navigation', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(() => route('A'));\n\t\tconst B = sinon.fake(groggy(() => route('B'), 1));\n\t\tconst loadStart = sinon.fake();\n\t\tconst loadEnd = sinon.fake();\n\t\tconst routeChange = sinon.fake();\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router\n\t\t\t\t\t\tonLoadStart={loadStart}\n\t\t\t\t\t\tonLoadEnd={loadEnd}\n\t\t\t\t\t\tonRouteChange={routeChange}\n\t\t\t\t\t>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\n\t\tnavigation.navigate('/b');\n\t\tawait sleep(1);\n\n\t\texpect(loadStart).to.have.been.calledWith('/b');\n\t\texpect(loadEnd).not.to.have.been.called;\n\t\texpect(routeChange).not.to.have.been.called;\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(loadStart).not.to.have.been.called;\n\t\texpect(loadEnd).to.have.been.calledWith('/b');\n\t\texpect(routeChange).to.have.been.calledWith('/b');\n\t});\n\n\tit('should only call onLoadEnd once upon promise flush', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst loadEnd = sinon.fake();\n\n\t\t/** @type {(string) => void} */\n\t\tlet set;\n\n\t\tconst App = () => {\n\t\t\tset = useState('1')[1];\n\t\t\treturn (\n\t\t\t\t<ErrorBoundary>\n\t\t\t\t\t<LocationProvider>\n\t\t\t\t\t\t<Router onLoadEnd={loadEnd}>\n\t\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t</Router>\n\t\t\t\t\t</LocationProvider>\n\t\t\t\t</ErrorBoundary>\n\t\t\t);\n\t\t}\n\t\trender(<App />,\tscratch);\n\n\t\tawait sleep(10);\n\n\t\texpect(loadEnd).to.have.been.calledWith('/');\n\t\tloadEnd.resetHistory();\n\n\t\tset('2');\n\t\tawait sleep(1);\n\n\t\texpect(loadEnd).not.to.have.been.called;\n\t});\n\n\tdescribe.skip('intercepted VS external links', () => {\n\t\tconst shouldIntercept = [null, '', '_self', 'self', '_SELF'];\n\t\tconst shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK'];\n\n\t\tconst clickHandler = sinon.fake(e => e.preventDefault());\n\n\t\tconst Route = sinon.fake(\n\t\t\t() => <div>\n\t\t\t\t{[...shouldIntercept, ...shouldNavigate].map((target, i) => {\n\t\t\t\t\tconst url = '/' + i + '/' + target;\n\t\t\t\t\tif (target === null) return <a href={url}>target = {target + ''}</a>;\n\t\t\t\t\treturn <a href={url} target={target}>target = {target}</a>;\n\t\t\t\t})}\n\t\t\t</div>\n\t\t);\n\n\t\tlet pushState;\n\n\t\tbefore(() => {\n\t\t\tpushState = sinon.spy(history, 'pushState');\n\t\t\taddEventListener('click', clickHandler);\n\t\t});\n\n\t\tafter(() => {\n\t\t\tpushState.restore();\n\t\t\tremoveEventListener('click', clickHandler);\n\t\t});\n\n\t\tbeforeEach(async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<Route default />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\t\t\tRoute.resetHistory();\n\t\t\tclickHandler.resetHistory();\n\t\t\tpushState.resetHistory();\n\t\t});\n\n\t\tconst getName = target => (target == null ? 'no target attribute' : `target=\"${target}\"`);\n\n\t\t// these should all be intercepted by the router.\n\t\tfor (const target of shouldIntercept) {\n\t\t\tit(`should intercept clicks on links with ${getName(target)}`, async () => {\n\t\t\t\tconst sel = target == null ? `a:not([target])` : `a[target=\"${target}\"]`;\n\t\t\t\tconst el = scratch.querySelector(sel);\n\t\t\t\tif (!el) throw Error(`Unable to find link: ${sel}`);\n\t\t\t\tconst url = el.getAttribute('href');\n\t\t\t\tel.click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(Route).to.have.been.calledOnce;\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\t\t\t});\n\t\t}\n\n\t\t// these should all navigate.\n\t\tfor (const target of shouldNavigate) {\n\t\t\tit(`should allow default browser navigation for links with ${getName(target)}`, async () => {\n\t\t\t\tconst sel = target == null ? `a:not([target])` : `a[target=\"${target}\"]`;\n\t\t\t\tconst el = scratch.querySelector(sel);\n\t\t\t\tif (!el) throw Error(`Unable to find link: ${sel}`);\n\t\t\t\tel.click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(Route).not.to.have.been.called;\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\t\t\t});\n\t\t}\n\t});\n\n\tdescribe.skip('intercepted VS external links with `scope`', () => {\n\t\tconst shouldIntercept = ['/app', '/app/deeper'];\n\t\tconst shouldNavigate = ['/site', '/site/deeper'];\n\n\t\tconst clickHandler = sinon.fake(e => e.preventDefault());\n\n\t\tconst Links = () => (\n\t\t\t<>\n\t\t\t\t<a href=\"/app\">Internal Link</a>\n\t\t\t\t<a href=\"/app/deeper\">Internal Deeper Link</a>\n\t\t\t\t<a href=\"/site\">External Link</a>\n\t\t\t\t<a href=\"/site/deeper\">External Deeper Link</a>\n\t\t\t</>\n\t\t);\n\n\t\tlet pushState;\n\n\t\tbefore(() => {\n\t\t\tpushState = sinon.spy(history, 'pushState');\n\t\t\taddEventListener('click', clickHandler);\n\t\t});\n\n\t\tafter(() => {\n\t\t\tpushState.restore();\n\t\t\tremoveEventListener('click', clickHandler);\n\t\t});\n\n\t\tbeforeEach(async () => {\n\t\t\tclickHandler.resetHistory();\n\t\t\tpushState.resetHistory();\n\t\t});\n\n\t\tit('should intercept clicks on links matching the `scope` props (string)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope=\"/app\">\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldIntercept) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should allow default browser navigation for links not matching the `scope` props (string)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope=\"app\">\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldNavigate) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should intercept clicks on links matching the `scope` props (regex)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope={/^\\/app/}>\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldIntercept) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should allow default browser navigation for links not matching the `scope` props (regex)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope={/^\\/app/}>\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldNavigate) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\t});\n\n\tit('should ignore clicks on document fragment links', async () => {\n\t\tconst Route = sinon.fake(\n\t\t\t() => <div>\n\t\t\t\t<a href=\"#foo\">just #foo</a>\n\t\t\t\t<a href=\"/other#bar\">other #bar</a>\n\t\t\t</div>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" />\n\t\t\t\t\t<Route path=\"/other\" />\n\t\t\t\t\t<Route default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tscratch.querySelector('a[href=\"#foo\"]').click();\n\t\tawait sleep(1);\n\n\t\t// NOTE: we don't (currently) propagate in-page anchor navigations into context, to avoid useless renders.\n\t\texpect(loc).to.deep.include({ url: '/' });\n\t\texpect(Route).not.to.have.been.called;\n\t\texpect(location.hash).to.equal('#foo');\n\n\t\tscratch.querySelector('a[href=\"/other#bar\"]').click();\n\t\tawait sleep(1);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\texpect(loc).to.deep.include({ url: '/other#bar', path: '/other' });\n\t\texpect(location.hash).to.equal('#bar');\n\t});\n\n\tit('should ignore clicks on download links', async () => {\n\t\tconst downloadHref = URL.createObjectURL(new Blob(['Hello World!'], { type: 'text/plain' }));\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<a href={downloadHref} download=\"hello-world.txt\">Download Me</a>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[download]').click();\n\t\tawait sleep(1);\n\n\t\t// If the router attempted to navigate, the page would throw a SecurityError\n\t\t// and the test would fail.\n\t\texpect(true).to.equal(true);\n\t});\n\n\tit('should normalize children', async () => {\n\t\tconst Route = sinon.fake(() => <a href=\"/foo#foo\">foo</a>);\n\n\t\tconst routes = ['/foo', '/bar'];\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t{routes.map(route => <Route path={route} />)}\n\t\t\t\t\t<Route default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tscratch.querySelector('a[href=\"/foo#foo\"]').click();\n\t\tawait sleep(10);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\texpect(loc).to.deep.include({ url: '/foo#foo', path: '/foo' });\n\t});\n\n\tit('should match nested routes', async () => {\n\t\tlet route;\n\t\tconst Inner = () => (\n\t\t\t<Router>\n\t\t\t\t<Route\n\t\t\t\t\tpath=\"/bob\"\n\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\troute = useRoute();\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Router>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/foo/:id/*\" component={Inner} />\n\t\t\t\t</Router>\n\t\t\t\t<a href=\"/foo/bar/bob\"></a>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[href=\"/foo/bar/bob\"]').click();\n\t\tawait sleep(1);\n\t\texpect(route).to.deep.include({ path: '/bob', params: { id: 'bar' } });\n\t});\n\n\tit('should append params in nested routes', async () => {\n\t\tlet params;\n\t\tconst Inner = () => (\n\t\t\t<Router>\n\t\t\t\t<Route\n\t\t\t\t\tpath=\"/bob\"\n\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\tparams = useRoute().params;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Router>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/foo/:id/*\" component={Inner} />\n\t\t\t\t</Router>\n\t\t\t\t<a href=\"/foo/bar/bob\"></a>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[href=\"/foo/bar/bob\"]').click();\n\t\tawait sleep(1);\n\t\texpect(params).to.deep.include({ id: 'bar' });\n\t});\n\n\tit('should replace the current URL', async () => {\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" component={() => null} />\n\t\t\t\t\t<Route path=\"/foo\" component={() => null} />\n\t\t\t\t\t<Route path=\"/bar\" component={() => null} />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tnavigation.navigate('/foo');\n\t\tnavigation.navigate('/bar', { history: 'replace' });\n\n\t\tconst entries = navigation.entries();\n\n\t\t// Top of the stack\n\t\tconst last = new URL(entries[entries.length - 1].url);\n\t\texpect(last.pathname).to.equal('/bar');\n\n\t\t// Entry before\n\t\tconst secondLast = new URL(entries[entries.length - 2].url);\n\t\texpect(secondLast.pathname).to.equal('/');\n\t});\n\n\tit('should support using `Router` as an implicit suspense boundary', async () => {\n\t\tlet data;\n\t\tfunction useSuspense() {\n\t\t\tconst [_, update] = useState();\n\n\t\t\tif (!data) {\n\t\t\t\tdata = new Promise(r => setTimeout(r, 5, 'data'));\n\t\t\t\tdata.then(\n\t\t\t\t\t(res) => update((data.res = res)),\n\t\t\t\t\t(err) => update((data.err = err))\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (data.res) return data.res;\n\t\t\tif (data.err) throw data.err;\n\t\t\tthrow data;\n\t\t}\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route\n\t\t\t\t\t\tpath=\"/\"\n\t\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\t\tconst result = useSuspense();\n\t\t\t\t\t\t\treturn <h1>{result}</h1>;\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', '');\n\t\tawait sleep(10);\n\t\texpect(scratch).to.have.property('textContent', 'data');\n\t});\n\n\tit('should intercept clicks on links inside open shadow DOM', async () => {\n\t\tconst shadowlink = document.createElement('a');\n\t\tshadowlink.href = '/shadow';\n\t\tshadowlink.textContent = 'Shadow Link';\n\n\t\tconst attachShadow = (el) => {\n\t\t\tif (!el || el.shadowRoot) return;\n\t\t\tconst shadowroot = el.attachShadow({ mode: 'open' });\n\t\t\tshadowroot.appendChild(shadowlink);\n\t\t}\n\n\t\tconst Home = sinon.fake(() => <div ref={attachShadow}></div>);\n\t\tconst Shadow = sinon.fake(() => <div>Shadow Route</div>);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" component={Home} />\n\t\t\t\t\t<Route path=\"/shadow\" component={Shadow}/>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tshadowlink.click();\n\n\t\tawait sleep(1);\n\n\t\texpect(loc).to.deep.include({ url: '/shadow' });\n\t\texpect(Shadow).to.have.been.calledOnce;\n\t\texpect(scratch).to.have.property('textContent', 'Shadow Route');\n\t});\n\n\tit('should not preserve param state after match failures', async () => {\n\t\tconst Params = () => {\n\t\t\tconst { params } = useRoute();\n\t\t\treturn <h1>{JSON.stringify(params)}</h1>\n\t\t};\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/category/:id\" component={Params} />\n\t\t\t\t\t<Route path=\"/category/:categoryId/products/new\" component={Params} />\n\t\t\t\t\t<Route path=\"/category/:categoryId/products/:id/edit\" component={Params} />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tnavigation.navigate('/category/123');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('textContent', '{\"id\":\"123\"}');\n\n\t\tnavigation.navigate('/category/123/products/new');\n\t\tawait sleep(10);\n\n\t\t// If the same `params` object was reused, this would also have an `id` property\n\t\t// from a failed partial match against the first route.\n\t\texpect(scratch).to.have.property('textContent', '{\"categoryId\":\"123\"}');\n\n\t\tnavigation.navigate('/category/123/products/456/edit');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('textContent', '{\"categoryId\":\"123\",\"id\":\"456\"}');\n\t});\n\n\tit('should support navigating backwards and forwards', async () => {\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" component={() => null} />\n\t\t\t\t\t<Route path=\"/foo\" component={() => null} />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tnavigation.navigate('/foo');\n\t\tawait sleep(10);\n\n\t\texpect(loc).to.deep.include({ url: '/foo', path: '/foo', query: {} });\n\n\t\tawait navigation.back().finished;\n\t\tawait sleep(10);\n\n\t\texpect(loc).to.deep.include({ url: '/', path: '/', query: {} });\n\n\t\tawait navigation.forward().finished;\n\t\tawait sleep(10);\n\n\t\texpect(loc).to.deep.include({ url: '/foo', path: '/foo', query: {} });\n\t});\n});\n\nconst MODE_HYDRATE = 1 << 5;\nconst MODE_SUSPENDED = 1 << 7;\n\ndescribe('hydration', () => {\n\tlet scratch;\n\n\tbeforeEach(() => {\n\t\tif (scratch) {\n\t\t\trender(null, scratch);\n\t\t\tscratch.remove();\n\t\t}\n\t\tscratch = document.createElement('scratch');\n\t\tdocument.body.appendChild(scratch);\n\t\thistory.replaceState(null, null, '/');\n\t});\n\n\tit('should wait for asynchronous routes', async () => {\n\t\tscratch.innerHTML = '<div><h1>A</h1><p>hello</p></div>';\n\t\tconst route = name => (\n\t\t\t<div>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</div>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\n\t\thydrate(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t</Router>\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\tconst mutations = [];\n\t\tconst mutationObserver = new MutationObserver((x) => {\n\t\t\tmutations.push(...x)\n\t\t});\n\t\tmutationObserver.observe(scratch, { childList: true, subtree: true });\n\n\t\texpect(scratch).to.have.property('innerHTML', '<div><h1>A</h1><p>hello</p></div>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\tconst oldOptionsVnode = options.__b;\n\t\tlet hasMatched = false;\n\t\toptions.__b = (vnode) => {\n\t\t\tif (vnode.type === A && !hasMatched) {\n\t\t\t\thasMatched = true;\n\t\t\t\tif (vnode.__ && vnode.__.__h) {\n\t\t\t\t\texpect(vnode.__.__h).to.equal(true)\n\t\t\t\t} else if (vnode.__ && vnode.__.__u) {\n\t\t\t\t\texpect(!!(vnode.__.__u & MODE_SUSPENDED)).to.equal(true);\n\t\t\t\t\texpect(!!(vnode.__.__u & MODE_HYDRATE)).to.equal(true);\n\t\t\t\t} else {\n\t\t\t\t\texpect(true).to.equal(false);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (oldOptionsVnode) {\n\t\t\t\toldOptionsVnode(vnode);\n\t\t\t}\n\t\t}\n\t\tA.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<div><h1>A</h1><p>hello</p></div>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(mutations).to.have.length(0);\n\n\t\toptions.__b = oldOptionsVnode;\n\t});\n})\n"
  },
  {
    "path": "test/router.test.js",
    "content": "import { h, Fragment, render, Component, hydrate, options } from 'preact';\nimport { useState } from 'preact/hooks';\nimport * as chai from 'chai';\nimport * as sinon from 'sinon';\nimport sinonChai from 'sinon-chai';\n\nimport { LocationProvider, Router, useLocation, Route, useRoute } from '../src/router.js';\nimport lazy, { ErrorBoundary } from '../src/lazy.js';\n\nimport './setup.js';\n\nconst expect = chai.expect;\nchai.use(sinonChai);\n\n/**\n * Usage:\n * - `await sleep(1)` for nav + loc/pushState/sync component check\n * - `await sleep(10)` for nav + async component check\n */\nconst sleep = ms => new Promise(r => setTimeout(r, ms));\n\n// delayed lazy()\nconst groggy = (component, ms) => lazy(() => sleep(ms).then(() => component));\n\ndescribe('Router', () => {\n\tlet scratch, loc;\n\n\tconst ShallowLocation = () => {\n\t\tloc = useLocation();\n\t\treturn null;\n\t}\n\n\tbeforeEach(() => {\n\t\tif (scratch) {\n\t\t\trender(null, scratch);\n\t\t\tscratch.remove();\n\t\t}\n\t\tloc = undefined;\n\t\tscratch = document.createElement('scratch');\n\t\tdocument.body.appendChild(scratch);\n\t\thistory.replaceState(null, null, '/');\n\t});\n\n\n\tit('should throw a clear error if the LocationProvider is missing', () => {\n\t\tconst Home = () => <h1>Home</h1>;\n\n\t\ttry {\n\t\t\trender(\n\t\t\t\t<Router>\n\t\t\t\t\t<Home path=\"/\" test=\"2\" />\n\t\t\t\t</Router>,\n\t\t\t\tscratch\n\t\t\t);\n\t\t\texpect.fail('should have thrown');\n\t\t} catch (e) {\n\t\t\texpect(e.message).to.include('must be used within a <LocationProvider>');\n\t\t}\n\t});\n\n\tit('should strip trailing slashes from path', async () => {\n\t\trender(\n\t\t\t<LocationProvider url=\"/a/\">\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/a/',\n\t\t\tpath: '/a',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should support class components using LocationProvider.ctx', () => {\n\t\tclass Foo extends Component {\n\t\t\tstatic contextType = LocationProvider.ctx;\n\n\t\t\trender() {\n\t\t\t\tloc = this.context;\n\t\t\t\treturn <h1>{loc.url}</h1>;\n\t\t\t}\n\t\t}\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Foo />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>/</h1>');\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should allow passing props to a route', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Home path=\"/\" test=\"2\" />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '2' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t});\n\n\tit('should allow updating props in a route', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\n\t\t/** @type {(string) => void} */\n\t\tlet set;\n\n\t\tconst App = () => {\n\t\t\tconst [test, setTest] = useState('2');\n\t\t\tset = setTest;\n\t\t\treturn (\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<Home path=\"/\" test={test} />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t);\n\t\t}\n\t\trender(<App />, scratch);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '2' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\n\t\tset('3')\n\t\tawait sleep(1);\n\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '', test: '3' });\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t});\n\n\tit('should switch between synchronous routes', async () => {\n\t\tconst Home = sinon.fake(() => <h1>Home</h1>);\n\t\tconst Profiles = sinon.fake(() => <h1>Profiles</h1>);\n\t\tconst Profile = sinon.fake(({ params }) => <h1>Profile: {params.id}</h1>);\n\t\tconst Fallback = sinon.fake(() => <h1>Fallback</h1>);\n\t\tconst stack = [];\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router onRouteChange={url => stack.push(url)}>\n\t\t\t\t\t<Home path=\"/\" />\n\t\t\t\t\t<Profiles path=\"/profiles\" />\n\t\t\t\t\t<Profile path=\"/profiles/:id\" />\n\t\t\t\t\t<Fallback default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', 'Home');\n\t\texpect(Home).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).not.to.have.been.called;\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/',\n\t\t\tpath: '/',\n\t\t\tquery: {},\n\t\t});\n\n\t\tHome.resetHistory();\n\t\tloc.route('/profiles');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Profiles');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).to.have.been.calledWith({ path: '/profiles', query: {}, params: {}, rest: '' });\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).not.to.have.been.called;\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/profiles',\n\t\t\tpath: '/profiles',\n\t\t\tquery: {}\n\t\t});\n\n\t\tProfiles.resetHistory();\n\t\tloc.route('/profiles/bob');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Profile: bob');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).to.have.been.calledWith(\n\t\t\t{ path: '/profiles/bob', query: {}, params: { id: 'bob' }, id: 'bob', rest: '' },\n\t\t);\n\t\texpect(Fallback).not.to.have.been.called;\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/profiles/bob',\n\t\t\tpath: '/profiles/bob',\n\t\t\tquery: {}\n\t\t});\n\n\t\tProfile.resetHistory();\n\t\tloc.route('/other?a=b&c=d');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('textContent', 'Fallback');\n\t\texpect(Home).not.to.have.been.called;\n\t\texpect(Profiles).not.to.have.been.called;\n\t\texpect(Profile).not.to.have.been.called;\n\t\texpect(Fallback).to.have.been.calledWith(\n\t\t\t{ default: true, path: '/other', query: { a: 'b', c: 'd' }, params: {}, rest: '' },\n\t\t);\n\n\t\texpect(loc).to.deep.include({\n\t\t\turl: '/other?a=b&c=d',\n\t\t\tpath: '/other',\n\t\t\tquery: { a: 'b', c: 'd' }\n\t\t});\n\t\texpect(stack).to.eql(['/profiles', '/profiles/bob', '/other?a=b&c=d']);\n\t});\n\n\tit('should wait for asynchronous routes', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst B = sinon.fake(groggy(() => route('B'), 1));\n\t\tconst C = sinon.fake(groggy(() => <h1>C</h1>, 1));\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b\" />\n\t\t\t\t\t\t<C path=\"/c\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tloc.route('/b');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).not.to.have.been.called;\n\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\t// We should never re-invoke <A /> while loading <B /> (that would be a remount of the old route):\n\t\texpect(A).not.to.have.been.called;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tB.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(B).to.have.been.calledOnce;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tB.resetHistory();\n\t\tloc.route('/c');\n\t\tloc.route('/c?1');\n\t\tloc.route('/c');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(B).not.to.have.been.called;\n\n\t\tawait sleep(1);\n\n\t\tloc.route('/c');\n\t\tloc.route('/c?2');\n\t\tloc.route('/c');\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\t// We should never re-invoke <B /> while loading <C /> (that would be a remount of the old route):\n\t\texpect(B).not.to.have.been.called;\n\t\texpect(C).to.have.been.calledWith({ path: '/c', query: {}, params: {}, rest: '' });\n\n\t\tC.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>C</h1>');\n\t\texpect(C).to.have.been.calledOnce;\n\t\texpect(C).to.have.been.calledWith({ path: '/c', query: {}, params: {}, rest: '' });\n\n\t\t// \"instant\" routing to already-loaded routes\n\n\t\tC.resetHistory();\n\t\tB.resetHistory();\n\t\tloc.route('/b');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(C).not.to.have.been.called;\n\t\texpect(B).to.have.been.calledOnce;\n\t\texpect(B).to.have.been.calledWith({ path: '/b', query: {}, params: {}, rest: '' });\n\n\t\tA.resetHistory();\n\t\tB.resetHistory();\n\t\tloc.route('/');\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(B).not.to.have.been.called;\n\t\texpect(A).to.have.been.calledOnce;\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t});\n\n\tit('rerenders same-component routes rather than swap', async () => {\n\t\tconst A = sinon.fake(() => <h1>a</h1>);\n\t\tconst B = sinon.fake(groggy(({ sub }) => <h1>b/{sub}</h1>, 1));\n\n\t\t// Counts the wrappers around route components to determine what the Router is returning\n\t\t// Count will be 2 for switching route components, and 2 more if the new route is lazily loaded\n\t\t// A same-route navigation adds 1\n\t\tlet renderRefCount = 0;\n\n\t\tconst old = options.vnode;\n\t\toptions.vnode = (vnode) => {\n\t\t\tif (typeof vnode.type === 'function' && vnode.props.r !== undefined) {\n\t\t\t\trenderRefCount += 1;\n\t\t\t}\n\n\t\t\tif (old) old(vnode);\n\t\t}\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b/:sub\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>a</h1>');\n\t\texpect(renderRefCount).to.equal(2);\n\n\t\trenderRefCount = 0;\n\t\tloc.route('/b/a');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>b/a</h1>');\n\t\texpect(renderRefCount).to.equal(4);\n\n\t\trenderRefCount = 0;\n\t\tloc.route('/b/b');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>b/b</h1>');\n\t\texpect(renderRefCount).to.equal(1);\n\n\t\trenderRefCount = 0;\n\t\tloc.route('/');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>a</h1>');\n\t\texpect(renderRefCount).to.equal(2);\n\n\t\toptions.vnode = old;\n\t});\n\n\tit('should support onLoadStart/onLoadEnd/onRouteChange w/out navigation', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst loadStart = sinon.fake();\n\t\tconst loadEnd = sinon.fake();\n\t\tconst routeChange = sinon.fake();\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router\n\t\t\t\t\t\tonLoadStart={loadStart}\n\t\t\t\t\t\tonLoadEnd={loadEnd}\n\t\t\t\t\t\tonRouteChange={routeChange}\n\t\t\t\t\t>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t</Router>\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('innerHTML', '');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(loadStart).to.have.been.calledWith('/');\n\t\texpect(loadEnd).not.to.have.been.called;\n\t\texpect(routeChange).not.to.have.been.called;\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\t\tawait sleep(1);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>A</h1><p>hello</p>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(loadStart).not.to.have.been.called;\n\t\texpect(loadEnd).to.have.been.calledWith('/');\n\t\texpect(routeChange).not.to.have.been.called;\n\t});\n\n\tit('should support onLoadStart/onLoadEnd/onRouteChange w/ navigation', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(() => route('A'));\n\t\tconst B = sinon.fake(groggy(() => route('B'), 1));\n\t\tconst loadStart = sinon.fake();\n\t\tconst loadEnd = sinon.fake();\n\t\tconst routeChange = sinon.fake();\n\n\t\trender(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router\n\t\t\t\t\t\tonLoadStart={loadStart}\n\t\t\t\t\t\tonLoadEnd={loadEnd}\n\t\t\t\t\t\tonRouteChange={routeChange}\n\t\t\t\t\t>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t<B path=\"/b\" />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\n\t\tloc.route('/b');\n\t\tawait sleep(1);\n\n\t\texpect(loadStart).to.have.been.calledWith('/b');\n\t\texpect(loadEnd).not.to.have.been.called;\n\t\texpect(routeChange).not.to.have.been.called;\n\n\t\tA.resetHistory();\n\t\tloadStart.resetHistory();\n\t\tloadEnd.resetHistory();\n\t\trouteChange.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<h1>B</h1><p>hello</p>');\n\t\texpect(loadStart).not.to.have.been.called;\n\t\texpect(loadEnd).to.have.been.calledWith('/b');\n\t\texpect(routeChange).to.have.been.calledWith('/b');\n\t});\n\n\tit('should only call onLoadEnd once upon promise flush', async () => {\n\t\tconst route = name => (\n\t\t\t<>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\t\tconst loadEnd = sinon.fake();\n\n\t\t/** @type {(string) => void} */\n\t\tlet set;\n\n\t\tconst App = () => {\n\t\t\tset = useState('1')[1];\n\t\t\treturn (\n\t\t\t\t<ErrorBoundary>\n\t\t\t\t\t<LocationProvider>\n\t\t\t\t\t\t<Router onLoadEnd={loadEnd}>\n\t\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t\t</Router>\n\t\t\t\t\t</LocationProvider>\n\t\t\t\t</ErrorBoundary>\n\t\t\t);\n\t\t}\n\t\trender(<App />,\tscratch);\n\n\t\tawait sleep(10);\n\n\t\texpect(loadEnd).to.have.been.calledWith('/');\n\t\tloadEnd.resetHistory();\n\n\t\tset('2');\n\t\tawait sleep(1);\n\n\t\texpect(loadEnd).not.to.have.been.called;\n\t});\n\n\tdescribe('intercepted VS external links', () => {\n\t\tconst shouldIntercept = [null, '', '_self', 'self', '_SELF'];\n\t\tconst shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK'];\n\n\t\tconst clickHandler = sinon.fake(e => e.preventDefault());\n\n\t\tconst Route = sinon.fake(\n\t\t\t() => <div>\n\t\t\t\t{[...shouldIntercept, ...shouldNavigate].map((target, i) => {\n\t\t\t\t\tconst url = '/' + i + '/' + target;\n\t\t\t\t\tif (target === null) return <a href={url}>target = {target + ''}</a>;\n\t\t\t\t\treturn <a href={url} target={target}>target = {target}</a>;\n\t\t\t\t})}\n\t\t\t</div>\n\t\t);\n\n\t\tlet pushState;\n\n\t\tbefore(() => {\n\t\t\tpushState = sinon.spy(history, 'pushState');\n\t\t\taddEventListener('click', clickHandler);\n\t\t});\n\n\t\tafter(() => {\n\t\t\tpushState.restore();\n\t\t\tremoveEventListener('click', clickHandler);\n\t\t});\n\n\t\tbeforeEach(async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<Route default />\n\t\t\t\t\t</Router>\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\t\t\tRoute.resetHistory();\n\t\t\tclickHandler.resetHistory();\n\t\t\tpushState.resetHistory();\n\t\t});\n\n\t\tconst getName = target => (target == null ? 'no target attribute' : `target=\"${target}\"`);\n\n\t\t// these should all be intercepted by the router.\n\t\tfor (const target of shouldIntercept) {\n\t\t\tit(`should intercept clicks on links with ${getName(target)}`, async () => {\n\t\t\t\tconst sel = target == null ? `a:not([target])` : `a[target=\"${target}\"]`;\n\t\t\t\tconst el = scratch.querySelector(sel);\n\t\t\t\tif (!el) throw Error(`Unable to find link: ${sel}`);\n\t\t\t\tconst url = el.getAttribute('href');\n\t\t\t\tel.click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(Route).to.have.been.calledOnce;\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\t\t\t});\n\t\t}\n\n\t\t// these should all navigate.\n\t\tfor (const target of shouldNavigate) {\n\t\t\tit(`should allow default browser navigation for links with ${getName(target)}`, async () => {\n\t\t\t\tconst sel = target == null ? `a:not([target])` : `a[target=\"${target}\"]`;\n\t\t\t\tconst el = scratch.querySelector(sel);\n\t\t\t\tif (!el) throw Error(`Unable to find link: ${sel}`);\n\t\t\t\tel.click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(Route).not.to.have.been.called;\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\t\t\t});\n\t\t}\n\t});\n\n\tdescribe('intercepted VS external links with `scope`', () => {\n\t\tconst shouldIntercept = ['/app', '/app/deeper'];\n\t\tconst shouldNavigate = ['/site', '/site/deeper'];\n\n\t\tconst clickHandler = sinon.fake(e => e.preventDefault());\n\n\t\tconst Links = () => (\n\t\t\t<>\n\t\t\t\t<a href=\"/app\">Internal Link</a>\n\t\t\t\t<a href=\"/app/deeper\">Internal Deeper Link</a>\n\t\t\t\t<a href=\"/site\">External Link</a>\n\t\t\t\t<a href=\"/site/deeper\">External Deeper Link</a>\n\t\t\t</>\n\t\t);\n\n\t\tlet pushState;\n\n\t\tbefore(() => {\n\t\t\tpushState = sinon.spy(history, 'pushState');\n\t\t\taddEventListener('click', clickHandler);\n\t\t});\n\n\t\tafter(() => {\n\t\t\tpushState.restore();\n\t\t\tremoveEventListener('click', clickHandler);\n\t\t});\n\n\t\tbeforeEach(async () => {\n\t\t\tclickHandler.resetHistory();\n\t\t\tpushState.resetHistory();\n\t\t});\n\n\t\tit('should intercept clicks on links matching the `scope` props (string)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope=\"/app\">\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldIntercept) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should allow default browser navigation for links not matching the `scope` props (string)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope=\"app\">\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldNavigate) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should intercept clicks on links matching the `scope` props (regex)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope={/^\\/app/}>\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldIntercept) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(loc).to.deep.include({ url });\n\t\t\t\texpect(pushState).to.have.been.calledWith(null, '', url);\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\n\t\tit('should allow default browser navigation for links not matching the `scope` props (regex)', async () => {\n\t\t\trender(\n\t\t\t\t<LocationProvider scope={/^\\/app/}>\n\t\t\t\t\t<Links />\n\t\t\t\t\t<ShallowLocation />\n\t\t\t\t</LocationProvider>,\n\t\t\t\tscratch\n\t\t\t);\n\n\t\t\tfor (const url of shouldNavigate) {\n\t\t\t\tscratch.querySelector(`a[href=\"${url}\"]`).click();\n\t\t\t\tawait sleep(1);\n\t\t\t\texpect(pushState).not.to.have.been.called;\n\t\t\t\texpect(clickHandler).to.have.been.called;\n\n\t\t\t\tpushState.resetHistory();\n\t\t\t\tclickHandler.resetHistory();\n\t\t\t}\n\t\t});\n\t});\n\n\tit('should scroll to top when navigating forward', async () => {\n\t\tconst scrollTo = sinon.spy(window, 'scrollTo');\n\n\t\tconst Route = sinon.fake(() => <div style={{ height: '1000px' }}><a href=\"/link\">link</a></div>);\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scrollTo).not.to.have.been.called;\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tloc.route('/programmatic');\n\t\tawait sleep(1);\n\n\t\texpect(loc).to.deep.include({ url: '/programmatic' });\n\t\texpect(scrollTo).to.have.been.calledWith(0, 0);\n\t\texpect(scrollTo).to.have.been.calledOnce;\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\t\tscrollTo.resetHistory();\n\n\t\tscratch.querySelector('a').click();\n\t\tawait sleep(1);\n\n\t\texpect(loc).to.deep.include({ url: '/link' });\n\t\texpect(scrollTo).to.have.been.calledWith(0, 0);\n\t\texpect(scrollTo).to.have.been.calledOnce;\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tscrollTo.restore();\n\t});\n\n\tit('should ignore clicks on document fragment links', async () => {\n\t\tconst pushState = sinon.spy(history, 'pushState');\n\n\t\tconst Route = sinon.fake(\n\t\t\t() => <div>\n\t\t\t\t<a href=\"#foo\">just #foo</a>\n\t\t\t\t<a href=\"/other#bar\">other #bar</a>\n\t\t\t</div>\n\t\t);\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" />\n\t\t\t\t\t<Route path=\"/other\" />\n\t\t\t\t\t<Route default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tscratch.querySelector('a[href=\"#foo\"]').click();\n\t\tawait sleep(1);\n\n\t\t// NOTE: we don't (currently) propagate in-page anchor navigations into context, to avoid useless renders.\n\t\texpect(loc).to.deep.include({ url: '/' });\n\t\texpect(Route).not.to.have.been.called;\n\t\texpect(pushState).not.to.have.been.called;\n\t\texpect(location.hash).to.equal('#foo');\n\n\t\tscratch.querySelector('a[href=\"/other#bar\"]').click();\n\t\tawait sleep(1);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\texpect(loc).to.deep.include({ url: '/other#bar', path: '/other' });\n\t\texpect(pushState).to.have.been.called;\n\t\texpect(location.hash).to.equal('#bar');\n\n\t\tpushState.restore();\n\t});\n\n\tit('should ignore clicks on download links', async () => {\n\t\tconst downloadHref = URL.createObjectURL(new Blob(['Hello World!'], { type: 'text/plain' }));\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<a href={downloadHref} download=\"hello-world.txt\">Download Me</a>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[download]').click();\n\t\tawait sleep(1);\n\n\t\t// If the router attempted to navigate, the page would throw a SecurityError\n\t\t// and the test would fail.\n\t\texpect(true).to.equal(true);\n\t});\n\n\tit('should normalize children', async () => {\n\t\tconst pushState = sinon.spy(history, 'pushState');\n\t\tconst Route = sinon.fake(() => <a href=\"/foo#foo\">foo</a>);\n\n\t\tconst routes = ['/foo', '/bar'];\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t{routes.map(route => <Route path={route} />)}\n\t\t\t\t\t<Route default />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\tRoute.resetHistory();\n\n\t\tscratch.querySelector('a[href=\"/foo#foo\"]').click();\n\t\tawait sleep(10);\n\n\t\texpect(Route).to.have.been.calledOnce;\n\t\texpect(loc).to.deep.include({ url: '/foo#foo', path: '/foo' });\n\t\texpect(pushState).to.have.been.called;\n\n\t\tpushState.restore();\n\t});\n\n\tit('should match nested routes', async () => {\n\t\tlet route;\n\t\tconst Inner = () => (\n\t\t\t<Router>\n\t\t\t\t<Route\n\t\t\t\t\tpath=\"/bob\"\n\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\troute = useRoute();\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Router>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/foo/:id/*\" component={Inner} />\n\t\t\t\t</Router>\n\t\t\t\t<a href=\"/foo/bar/bob\"></a>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[href=\"/foo/bar/bob\"]').click();\n\t\tawait sleep(1);\n\t\texpect(route).to.deep.include({ path: '/bob', params: { id: 'bar' } });\n\t});\n\n\tit('should append params in nested routes', async () => {\n\t\tlet params;\n\t\tconst Inner = () => (\n\t\t\t<Router>\n\t\t\t\t<Route\n\t\t\t\t\tpath=\"/bob\"\n\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\tparams = useRoute().params;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Router>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/foo/:id/*\" component={Inner} />\n\t\t\t\t</Router>\n\t\t\t\t<a href=\"/foo/bar/bob\"></a>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[href=\"/foo/bar/bob\"]').click();\n\t\tawait sleep(1);\n\t\texpect(params).to.deep.include({ id: 'bar' });\n\t});\n\n\tit('should not double-decode percent-encoded characters in nested routes', async () => {\n\t\tlet route;\n\t\tconst Inner = () => (\n\t\t\t<Router>\n\t\t\t\t<Route\n\t\t\t\t\tpath=\"/child/:id\"\n\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\troute = useRoute();\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Router>\n\t\t);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/nested/*\" component={Inner} />\n\t\t\t\t</Router>\n\t\t\t\t<a href=\"/nested/child/%25\"></a>\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tscratch.querySelector('a[href=\"/nested/child/%25\"]').click();\n\t\tawait sleep(1);\n\t\texpect(route).to.deep.include({ params: { id: '%' } });\n\t});\n\n\tit('should replace the current URL', async () => {\n\t\tconst pushState = sinon.spy(history, 'pushState');\n\t\tconst replaceState = sinon.spy(history, 'replaceState');\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/foo\" component={() => null} />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tloc.route(\"/foo\", true);\n\t\texpect(pushState).not.to.have.been.called;\n\t\texpect(replaceState).to.have.been.calledWith(null, \"\", \"/foo\");\n\n\t\tpushState.restore();\n\t\treplaceState.restore();\n\t});\n\n\tit('should support using `Router` as an implicit suspense boundary', async () => {\n\t\tlet data;\n\t\tfunction useSuspense() {\n\t\t\tconst [_, update] = useState();\n\n\t\t\tif (!data) {\n\t\t\t\tdata = new Promise(r => setTimeout(r, 5, 'data'));\n\t\t\t\tdata.then(\n\t\t\t\t\t(res) => update((data.res = res)),\n\t\t\t\t\t(err) => update((data.err = err))\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (data.res) return data.res;\n\t\t\tif (data.err) throw data.err;\n\t\t\tthrow data;\n\t\t}\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route\n\t\t\t\t\t\tpath=\"/\"\n\t\t\t\t\t\tcomponent={() => {\n\t\t\t\t\t\t\tconst result = useSuspense();\n\t\t\t\t\t\t\treturn <h1>{result}</h1>;\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\texpect(scratch).to.have.property('textContent', '');\n\t\tawait sleep(10);\n\t\texpect(scratch).to.have.property('textContent', 'data');\n\t});\n\n\tit('should intercept clicks on links inside open shadow DOM', async () => {\n\t\tconst shadowlink = document.createElement('a');\n\t\tshadowlink.href = '/shadow';\n\t\tshadowlink.textContent = 'Shadow Link';\n\t\tshadowlink.addEventListener('click', e => e.preventDefault());\n\n\t\tconst attachShadow = (el) => {\n\t\t\tif (!el || el.shadowRoot) return;\n\t\t\tconst shadowroot = el.attachShadow({ mode: 'open' });\n\t\t\tshadowroot.appendChild(shadowlink);\n\t\t}\n\n\t\tconst Home = sinon.fake(() => <div ref={attachShadow}></div>);\n\t\tconst Shadow = sinon.fake(() => <div>Shadow Route</div>);\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/\" component={Home} />\n\t\t\t\t\t<Route path=\"/shadow\" component={Shadow}/>\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tshadowlink.click();\n\n\t\tawait sleep(1);\n\n\t\texpect(loc).to.deep.include({ url: '/shadow' });\n\t\texpect(Shadow).to.have.been.calledOnce;\n\t\texpect(scratch).to.have.property('textContent', 'Shadow Route');\n\t});\n\n\tit('should not preserve param state after match failures', async () => {\n\t\tconst Params = () => {\n\t\t\tconst { params } = useRoute();\n\t\t\treturn <h1>{JSON.stringify(params)}</h1>\n\t\t};\n\n\t\trender(\n\t\t\t<LocationProvider>\n\t\t\t\t<Router>\n\t\t\t\t\t<Route path=\"/category/:id\" component={Params} />\n\t\t\t\t\t<Route path=\"/category/:categoryId/products/new\" component={Params} />\n\t\t\t\t\t<Route path=\"/category/:categoryId/products/:id/edit\" component={Params} />\n\t\t\t\t</Router>\n\t\t\t\t<ShallowLocation />\n\t\t\t</LocationProvider>,\n\t\t\tscratch\n\t\t);\n\n\t\tloc.route('/category/123');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('textContent', '{\"id\":\"123\"}');\n\n\t\tloc.route('/category/123/products/new');\n\t\tawait sleep(10);\n\n\t\t// If the same `params` object was reused, this would also have an `id` property\n\t\t// from a failed partial match against the first route.\n\t\texpect(scratch).to.have.property('textContent', '{\"categoryId\":\"123\"}');\n\n\t\tloc.route('/category/123/products/456/edit');\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('textContent', '{\"categoryId\":\"123\",\"id\":\"456\"}');\n\t});\n});\n\nconst MODE_HYDRATE = 1 << 5;\nconst MODE_SUSPENDED = 1 << 7;\n\ndescribe('hydration', () => {\n\tlet scratch;\n\n\tbeforeEach(() => {\n\t\tif (scratch) {\n\t\t\trender(null, scratch);\n\t\t\tscratch.remove();\n\t\t}\n\t\tscratch = document.createElement('scratch');\n\t\tdocument.body.appendChild(scratch);\n\t\thistory.replaceState(null, null, '/');\n\t});\n\n\tit('should wait for asynchronous routes', async () => {\n\t\tscratch.innerHTML = '<div><h1>A</h1><p>hello</p></div>';\n\t\tconst route = name => (\n\t\t\t<div>\n\t\t\t\t<h1>{name}</h1>\n\t\t\t\t<p>hello</p>\n\t\t\t</div>\n\t\t);\n\t\tconst A = sinon.fake(groggy(() => route('A'), 1));\n\n\t\thydrate(\n\t\t\t<ErrorBoundary>\n\t\t\t\t<LocationProvider>\n\t\t\t\t\t<Router>\n\t\t\t\t\t\t<A path=\"/\" />\n\t\t\t\t\t</Router>\n\t\t\t\t</LocationProvider>\n\t\t\t</ErrorBoundary>,\n\t\t\tscratch\n\t\t);\n\n\t\tconst mutations = [];\n\t\tconst mutationObserver = new MutationObserver((x) => {\n\t\t\tmutations.push(...x)\n\t\t});\n\t\tmutationObserver.observe(scratch, { childList: true, subtree: true });\n\n\t\texpect(scratch).to.have.property('innerHTML', '<div><h1>A</h1><p>hello</p></div>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\tconst oldOptionsVnode = options.__b;\n\t\tlet hasMatched = false;\n\t\toptions.__b = (vnode) => {\n\t\t\tif (vnode.type === A && !hasMatched) {\n\t\t\t\thasMatched = true;\n\t\t\t\tif (vnode.__ && vnode.__.__h) {\n\t\t\t\t\texpect(vnode.__.__h).to.equal(true)\n\t\t\t\t} else if (vnode.__ && vnode.__.__u) {\n\t\t\t\t\texpect(!!(vnode.__.__u & MODE_SUSPENDED)).to.equal(true);\n\t\t\t\t\texpect(!!(vnode.__.__u & MODE_HYDRATE)).to.equal(true);\n\t\t\t\t} else {\n\t\t\t\t\texpect(true).to.equal(false);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (oldOptionsVnode) {\n\t\t\t\toldOptionsVnode(vnode);\n\t\t\t}\n\t\t}\n\t\tA.resetHistory();\n\t\tawait sleep(10);\n\n\t\texpect(scratch).to.have.property('innerHTML', '<div><h1>A</h1><p>hello</p></div>');\n\t\texpect(A).to.have.been.calledWith({ path: '/', query: {}, params: {}, rest: '' });\n\t\texpect(mutations).to.have.length(0);\n\n\t\toptions.__b = oldOptionsVnode;\n\t});\n})\n"
  },
  {
    "path": "test/setup.js",
    "content": "import kl from 'kleur';\n\n/*\n * Custom serializer borrowed from the Preact repo as the default in wtr\n * is busted when it comes to cyclical references or objects beyond a\n * certain depth.\n\n * This also has the benefit of being much prettier and human readable,\n * includes indentation, coloring, and support for Map and Set objects.\n */\nfunction patchConsole(method) {\n\tconst original = window.console[method];\n\twindow.console[method] = (...args) => {\n\t\toriginal.apply(window.console, serializeConsoleArgs(args));\n\t};\n}\n\npatchConsole('log');\npatchConsole('warn');\npatchConsole('error');\npatchConsole('info');\n\n/**\n * @param {any[]} args\n * @returns {[string]}\n */\nfunction serializeConsoleArgs(args) {\n\tconst flat = args.map(arg => serialize(arg, 'flat', 0, new Set()));\n\t// We don't have access to the users terminal width, so we'll try to\n\t// format everything into one line if possible and assume a terminal\n\t// width of 80 chars\n\tif (kl.reset(flat.join(', ')).length <= 80) {\n\t\treturn [flat.join(', ')];\n\t}\n\n\tconst serialized = args.map(arg => serialize(arg, 'default', 0, new Set()));\n\treturn ['\\n' + serialized.join(',\\n') + '\\n'];\n}\n\n/**\n * @param {number} n\n * @returns {string}\n */\nfunction applyIndent(n) {\n\tif (n <= 0) return '';\n\treturn '  '.repeat(n);\n}\n\n/**\n * @param {any} value\n * @param {\"flat\" | \"default\"} mode\n * @param {number} indent\n * @param {Set<any>} seen\n * @returns {string}\n */\nfunction serialize(value, mode, indent, seen) {\n\tif (seen.has(value)) {\n\t\treturn kl.cyan('[Circular]');\n\t}\n\n\tif (value === null) {\n\t\treturn kl.bold('null');\n\t} else if (Array.isArray(value)) {\n\t\tseen.add(value);\n\t\tconst values = value.map(v => serialize(v, mode, indent + 1, seen));\n\t\tif (mode === 'flat') {\n\t\t\treturn `[ ${values.join(', ')} ]`;\n\t\t}\n\n\t\tconst space = applyIndent(indent);\n\t\tconst pretty = values.map(v => applyIndent(indent + 1) + v).join(',\\n');\n\t\treturn `[\\n${pretty}\\n${space}]`;\n\t} else if (value instanceof Set) {\n\t\tconst values = [];\n\t\tvalue.forEach(v => {\n\t\t\tvalues.push(serialize(v, mode, indent, seen));\n\t\t});\n\n\t\tif (mode === 'flat') {\n\t\t\treturn `Set(${value.size}) { ${values.join(', ')} }`;\n\t\t}\n\n\t\tconst pretty = values.map(v => applyIndent(indent + 1) + v).join(',\\n');\n\t\treturn `Set(${value.size}) {\\n${pretty}\\n${applyIndent(indent)}}`;\n\t} else if (value instanceof Map) {\n\t\tconst values = [];\n\t\tvalue.forEach((v, k) => {\n\t\t\tvalues.push([\n\t\t\t\tserialize(v, 'flat', indent, seen),\n\t\t\t\tserialize(k, 'flat', indent, seen)\n\t\t\t]);\n\t\t});\n\n\t\tif (mode === 'flat') {\n\t\t\tconst pretty = values.map(v => `${v[0]} => ${v[1]}`).join(', ');\n\t\t\treturn `Map(${value.size}) { ${pretty} }`;\n\t\t}\n\n\t\tconst pretty = values\n\t\t\t.map(v => {\n\t\t\t\treturn applyIndent(indent + 1) + `${v[0]} => ${v[1]}`;\n\t\t\t})\n\t\t\t.join(', ');\n\t\treturn `Map(${value.size}) {\\n${pretty}\\n${applyIndent(indent)}}`;\n\t}\n\n\tswitch (typeof value) {\n\t\tcase 'undefined':\n\t\t\treturn kl.dim('undefined');\n\n\t\tcase 'bigint':\n\t\tcase 'number':\n\t\tcase 'boolean':\n\t\t\treturn kl.yellow(String(value));\n\t\tcase 'string': {\n\t\t\t// By default node's built in logging doesn't wrap top level\n\t\t\t// strings with quotes\n\t\t\tif (indent === 0) {\n\t\t\t\treturn String(value);\n\t\t\t}\n\t\t\tconst quote = /[^\\\\]\"/.test(value) ? '\"' : \"'\";\n\t\t\treturn kl.green(String(quote + value + quote));\n\t\t}\n\t\tcase 'symbol':\n\t\t\treturn kl.green(value.toString());\n\t\tcase 'function':\n\t\t\treturn kl.cyan(`[Function: ${value.name || 'anonymous'}]`);\n\t}\n\n\tif (value instanceof Element) {\n\t\treturn value.outerHTML;\n\t}\n\tif (value instanceof Error) {\n\t\treturn value.stack;\n\t}\n\n\tseen.add(value);\n\n\tconst props = Object.keys(value).map(key => {\n\t\t// Skip calling getters\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, key);\n\t\tif (typeof desc.get === 'function') {\n\t\t\treturn `get ${key}()`;\n\t\t}\n\n\t\tconst v = serialize(value[key], mode, indent + 1, seen);\n\t\treturn `${key}: ${v}`;\n\t});\n\n\tif (props.length === 0) {\n\t\treturn '{}';\n\t} else if (mode === 'flat') {\n\t\tconst pretty = props.join(', ');\n\t\treturn `{ ${pretty} }`;\n\t}\n\n\tconst pretty = props.map(p => applyIndent(indent + 1) + p).join(',\\n');\n\treturn `{\\n${pretty}\\n${applyIndent(indent)}}`;\n}\n"
  },
  {
    "path": "web-test-runner.config.js",
    "content": "import { esbuildPlugin } from \"@web/dev-server-esbuild\";\n\nexport default {\n\tnodeResolve: true,\n\ttestsFinishTimeout: 30000,\n\tplugins: [\n\t\tesbuildPlugin({\n\t\t\tjsx: true,\n\t\t\tloaders: {  '.js': 'jsx' },\n\t\t\tjsxFactory: 'h',\n\t\t\tjsxFragment: 'Fragment',\n\t\t}),\n\t],\n};\n"
  }
]