This commit is contained in:
2025-06-16 13:37:14 +02:00
parent ac273655e6
commit a8b82208f7
5100 changed files with 737524 additions and 2 deletions

21
node_modules/preact-iso/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 The Preact Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

383
node_modules/preact-iso/README.md generated vendored Normal file
View File

@@ -0,0 +1,383 @@
# preact-iso
[![Preact Slack Community](https://img.shields.io/badge/slack-Preact%20Slack%20Community-blue?logo=slack)](https://chat.preactjs.com/)
Isomorphic async tools for Preact.
- Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration.
- Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies.
- Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions.
## Routing
`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.
```js
import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';
// Synchronous
import Home from './routes/home.js';
// Asynchronous (throws a promise)
const Profiles = lazy(() => import('./routes/profiles.js'));
const Profile = lazy(() => import('./routes/profile.js'));
const NotFound = lazy(() => import('./routes/_404.js'));
const App = () => (
<LocationProvider>
<ErrorBoundary>
<Router>
<Home path="/" />
{/* Alternative dedicated route component for better TS support */}
<Route path="/profiles" component={Profiles} />
<Route path="/profile/:id" component={Profile} />
{/* `default` prop indicates a fallback route. Useful for 404 pages */}
<NotFound default />
</Router>
</ErrorBoundary>
</LocationProvider>
);
```
**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.
**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.
## Prerendering
`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.
Primarily 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.
```js
import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso';
// Asynchronous (throws a promise)
const Foo = lazy(() => import('./foo.js'));
const App = () => (
<LocationProvider>
<ErrorBoundary>
<Router>
<Foo path="/" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
hydrate(<App />);
export async function prerender(data) {
return await ssr(<App />);
}
```
## Nested Routing
Some 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.
Partially 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.
```js
import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';
import AllMovies from './routes/movies/all.js';
const NotFound = lazy(() => import('./routes/_404.js'));
const App = () => (
<LocationProvider>
<ErrorBoundary>
<Router>
<Router path="/movies" component={AllMovies} />
<Route path="/movies/*" component={Movies} />
<NotFound default />
</Router>
</ErrorBoundary>
</LocationProvider>
);
const TrendingMovies = lazy(() => import('./routes/movies/trending.js'));
const SearchMovies = lazy(() => import('./routes/movies/search.js'));
const MovieDetails = lazy(() => import('./routes/movies/details.js'));
const Movies = () => (
<ErrorBoundary>
<Router>
<Route path="/trending" component={TrendingMovies} />
<Route path="/search" component={SearchMovies} />
<Route path="/:id" component={MovieDetails} />
</Router>
</ErrorBoundary>
);
```
The `<Movies>` component will be used for the following routes:
- `/movies/trending`
- `/movies/search`
- `/movies/Inception`
- `/movies/...`
It will not be used for any of the following:
- `/movies`
- `/movies/`
---
## API Docs
### `LocationProvider`
A context provider that provides the current location to its children. This is required for the router to function.
Props:
- `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.
Typically, you would wrap your entire app in this provider:
```js
import { LocationProvider } from 'preact-iso';
const App = () => (
<LocationProvider scope="/app">
{/* Your app here */}
</LocationProvider>
);
```
### `Router`
Props:
- `onRouteChange?: (url: string) => void` - Callback to be called when a route changes.
- `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.
- `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.
```js
import { LocationProvider, Router } from 'preact-iso';
const App = () => (
<LocationProvider>
<Router
onRouteChange={(url) => console.log('Route changed to', url)}
onLoadStart={(url) => console.log('Starting to load', url)}
onLoadEnd={(url) => console.log('Finished loading', url)}
>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
```
### `Route`
There are two ways to define routes using `preact-iso`:
1. Append router params to the route components directly: `<Home path="/" />`
2. Use the `Route` component instead: `<Route path="/" component={Home} />`
Appending 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.
TS 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.
While `<Home path="/" />` is completely equivalent to `<Route path="/" component={Home} />`, TS users may find the latter preferable.
```js
import { LocationProvider, Router, Route } from 'preact-iso';
const App = () => (
<LocationProvider>
<Router>
{/* Both of these are equivalent */}
<Home path="/" />
<Route path="/" component={Home} />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
<NotFound default />
</Router>
</LocationProvider>
);
```
Props for any route component:
- `path: string` - The path to match (read on)
- `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches
Specific to the `Route` component:
- `component: AnyComponent` - The component to render when the route matches
#### Path Segment Matching
Paths are matched using a simple string matching algorithm. The following features may be used:
- `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`)
- `/profile/:id` will match `/profile/123` and `/profile/abc`
- `/profile/:id?` will match `/profile` and `/profile/123`
- `/profile/:id*` will match `/profile`, `/profile/123`, and `/profile/123/abc`
- `/profile/:id+` will match `/profile/123`, `/profile/123/abc`
- `*` - Matches one or more URL segments
- `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc.
These can then be composed to create more complex routes:
- `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc.
The 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.
- `/profile/:id*`, with `/profile/123/abc`
- `id` is `123/abc`
- `/profile/:id/*`, with `/profile/123/abc`
- `id` is `123`
### `useLocation`
A hook to work with the `LocationProvider` to access location context.
Returns an object with the following properties:
- `url: string` - The current path & search params
- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `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.
### `useRoute`
A hook to access current route information. Unlike `useLocation`, this hook only works within `<Router>` components.
Returns an object with the following properties:
- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `params: Record<string, string>` - The current route parameters (`/profile/:id` -> `{ id: '123' }`)
### `lazy`
Make a lazily-loaded version of a Component.
`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.
```js
import { lazy, LocationProvider, Router } from 'preact-iso';
// Synchronous, not code-splitted:
import Home from './routes/home.js';
// Asynchronous, code-splitted:
const Profiles = lazy(() => import('./routes/profiles.js').then(m => m.Profiles)); // Expects a named export called `Profiles`
const Profile = lazy(() => import('./routes/profile.js')); // Expects a default export
const App = () => (
<LocationProvider>
<Router>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
```
The 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.
```js
const Profile = lazy(() => import('./routes/profile.js'));
function Home() {
return (
<a href="/profile/rschristian" onMouseOver={() => Profile.preload()}>
Profile Page -- Hover over me to preload the module!
</a>
);
}
```
### `ErrorBoundary`
A simple component to catch errors in the component tree below it.
Props:
- `onError?: (error: Error) => void` - A callback to be called when an error is caught
```js
import { LocationProvider, ErrorBoundary, Router } from 'preact-iso';
const App = () => (
<LocationProvider>
<ErrorBoundary onError={(e) => console.log(e)}>
<Router>
<Home path="/" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
```
### `hydrate`
A 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.
Pairs with the `prerender()` function.
Params:
- `jsx: ComponentChild` - The JSX element or component to render
- `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided.
```js
import { hydrate } from 'preact-iso';
const App = () => (
<div class="app">
<h1>Hello World</h1>
</div>
);
hydrate(<App />);
```
However, it is just a simple utility method. By no means is it essential to use, you can always use Preact's `hydrate` export directly.
### `prerender`
Renders 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 URL strings found in links on the generated page.
Pairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration)'s prerendering.
Params:
- `jsx: ComponentChild` - The JSX element or component to render
```js
import { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso';
// Asynchronous (throws a promise)
const Foo = lazy(() => import('./foo.js'));
const Bar = lazy(() => import('./bar.js'));
const App = () => (
<LocationProvider>
<ErrorBoundary>
<Router>
<Foo path="/" />
<Bar path="/bar" />
</Router>
</ErrorBoundary>
</LocationProvider>
);
const { html, links } = await prerender(<App />);
```
## License
[MIT](./LICENSE)

55
node_modules/preact-iso/package.json generated vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "preact-iso",
"version": "2.9.2",
"type": "module",
"main": "src/index.js",
"module": "src/index.js",
"types": "src/index.d.ts",
"exports": {
".": "./src/index.js",
"./router": "./src/router.js",
"./lazy": "./src/lazy.js",
"./prerender": "./src/prerender.js",
"./hydrate": "./src/hydrate.js"
},
"license": "MIT",
"description": "Isomorphic utilities for Preact",
"author": "The Preact Authors (https://preactjs.com)",
"repository": {
"type": "git",
"url": "git+https://github.com/preactjs/preact-iso.git"
},
"files": [
"src",
"!src/internal.d.ts",
"LICENSE",
"package.json",
"README.md"
],
"scripts": {
"test": "npm run test:node && npm run test:browser",
"test:browser": "wtr test/*.test.js",
"test:node": "uvu test/node"
},
"peerDependencies": {
"preact": ">=10",
"preact-render-to-string": ">=6.4.0"
},
"devDependencies": {
"@types/mocha": "^10.0.7",
"@types/sinon-chai": "^3.2.12",
"@web/dev-server-esbuild": "^1.0.2",
"@web/test-runner": "^0.18.3",
"chai": "^5.1.1",
"htm": "^3.1.1",
"kleur": "^4.1.5",
"preact": "^10.26.5",
"preact-render-to-string": "^6.5.11",
"sinon": "^18.0.0",
"sinon-chai": "^4.0.0",
"uvu": "^0.5.6"
},
"overrides": {
"@web/dev-server-core": "0.7.1"
}
}

3
node_modules/preact-iso/src/hydrate.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import { ComponentChild, ContainerNode } from 'preact';
export default function hydrate(jsx: ComponentChild, parent?: ContainerNode): void;

17
node_modules/preact-iso/src/hydrate.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
import { render, hydrate as hydrativeRender } from 'preact';
let initialized;
/** @type {typeof hydrativeRender} */
export default function hydrate(jsx, parent) {
if (typeof window === 'undefined') return;
let isodata = document.querySelector('script[type=isodata]');
// @ts-ignore-next
parent = parent || (isodata && isodata.parentNode) || document.body;
if (!initialized && isodata) {
hydrativeRender(jsx, parent);
} else {
render(jsx, parent);
}
initialized = true;
}

4
node_modules/preact-iso/src/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export { default as prerender } from './prerender.js';
export * from './router.js';
export { default as lazy, ErrorBoundary } from './lazy.js';
export { default as hydrate } from './hydrate.js';

7
node_modules/preact-iso/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
export { Router, LocationProvider, useLocation, Route, useRoute } from './router.js';
export { default as lazy, ErrorBoundary } from './lazy.js';
export { default as hydrate } from './hydrate.js';
export function prerender(vnode, options) {
return import('./prerender.js').then(m => m.default(vnode, options));
}

7
node_modules/preact-iso/src/lazy.d.ts generated vendored Normal file
View File

@@ -0,0 +1,7 @@
import { ComponentChildren, VNode } from 'preact';
export default function lazy<T>(load: () => Promise<{ default: T } | T>): T & {
preload: () => Promise<T>;
};
export function ErrorBoundary(props: { children?: ComponentChildren; onError?: (error: Error) => void }): VNode;

65
node_modules/preact-iso/src/lazy.js generated vendored Normal file
View File

@@ -0,0 +1,65 @@
import { h, options } from 'preact';
import { useState, useRef } from 'preact/hooks';
const oldDiff = options.__b;
options.__b = (vnode) => {
if (vnode.type && vnode.type._forwarded && vnode.ref) {
vnode.props.ref = vnode.ref;
vnode.ref = null;
}
if (oldDiff) oldDiff(vnode);
};
export default function lazy(load) {
let p, c;
const loadModule = () =>
load().then(m => (c = (m && m.default) || m));
const LazyComponent = props => {
const [, update] = useState(0);
const r = useRef(c);
if (!p) p = loadModule();
if (c !== undefined) return h(c, props);
if (!r.current) r.current = p.then(() => update(1));
throw p;
};
LazyComponent.preload = () => {
if (!p) p = loadModule();
return p;
}
LazyComponent._forwarded = true;
return LazyComponent;
}
// See https://github.com/preactjs/preact/blob/88680e91ec0d5fc29d38554a3e122b10824636b6/compat/src/suspense.js#L5
const oldCatchError = options.__e;
options.__e = (err, newVNode, oldVNode) => {
if (err && err.then) {
let v = newVNode;
while ((v = v.__)) {
if (v.__c && v.__c.__c) {
if (newVNode.__e == null) {
newVNode.__c.__z = [oldVNode.__e];
newVNode.__e = oldVNode.__e; // ._dom
newVNode.__k = oldVNode.__k; // ._children
}
if (!newVNode.__k) newVNode.__k = [];
return v.__c.__c(err, newVNode);
}
}
}
if (oldCatchError) oldCatchError(err, newVNode, oldVNode);
};
export function ErrorBoundary(props) {
this.__c = childDidSuspend;
this.componentDidCatch = props.onError;
return props.children;
}
function childDidSuspend(err) {
err.then(() => this.forceUpdate());
}

17
node_modules/preact-iso/src/prerender.d.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
import { VNode } from 'preact';
export interface PrerenderOptions {
props?: Record<string, unknown>;
}
export interface PrerenderResult {
html: string;
links?: Set<string>
}
export default function prerender(
vnode: VNode,
options?: PrerenderOptions
): Promise<PrerenderResult>;
export function locationStub(path: string): void;

59
node_modules/preact-iso/src/prerender.js generated vendored Normal file
View File

@@ -0,0 +1,59 @@
import { h, options, cloneElement } from 'preact';
import { renderToStringAsync } from 'preact-render-to-string';
let vnodeHook;
const old = options.vnode;
options.vnode = vnode => {
if (old) old(vnode);
if (vnodeHook) vnodeHook(vnode);
};
/**
* @param {ReturnType<h>} vnode The root JSX element to render (eg: `<App />`)
* @param {object} [options]
* @param {object} [options.props] Additional props to merge into the root JSX element
*/
export default async function prerender(vnode, options) {
options = options || {};
const props = options.props;
if (typeof vnode === 'function') {
vnode = h(vnode, props);
} else if (props) {
vnode = cloneElement(vnode, props);
}
let links = new Set();
vnodeHook = ({ type, props }) => {
if (type === 'a' && props && props.href && (!props.target || props.target === '_self')) {
links.add(props.href);
}
};
try {
let html = await renderToStringAsync(vnode);
html += `<script type="isodata"></script>`;
return { html, links };
} finally {
vnodeHook = null;
}
}
/**
* Update `location` to current URL so routers can use things like `location.pathname`
*
* @param {string} path - current URL path
*/
export function locationStub(path) {
globalThis.location = {};
const u = new URL(path, 'http://localhost');
for (const i in u) {
try {
globalThis.location[i] = /to[A-Z]/.test(i)
? u[i].bind(u)
: String(u[i]);
} catch {}
}
}

69
node_modules/preact-iso/src/router.d.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
import { AnyComponent, ComponentChildren, Context, VNode } from 'preact';
export const LocationProvider: {
(props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode;
ctx: Context<LocationHook>;
};
type NestedArray<T> = Array<T | NestedArray<T>>;
interface KnownProps {
path: string;
query: Record<string, string>;
params: Record<string, string>;
default?: boolean;
rest?: string;
component?: AnyComponent;
}
interface ArbitraryProps {
[prop: string]: any;
}
type MatchProps = KnownProps & ArbitraryProps;
/**
* Check if a URL path matches against a URL path pattern.
*
* Warning: This is largely an internal API, it may change in the future
* @param url - URL path (e.g. /user/12345)
* @param route - URL pattern (e.g. /user/:id)
*/
export function exec(url: string, route: string, matches?: MatchProps): MatchProps
export function Router(props: {
onRouteChange?: (url: string) => void;
onLoadEnd?: (url: string) => void;
onLoadStart?: (url: string) => void;
children?: NestedArray<VNode>;
}): VNode;
interface LocationHook {
url: string;
path: string;
query: Record<string, string>;
route: (url: string, replace?: boolean) => void;
}
export const useLocation: () => LocationHook;
interface RouteHook {
path: string;
query: Record<string, string>;
params: Record<string, string>;
}
export const useRoute: () => RouteHook;
type RoutableProps =
| { path: string; default?: false; }
| { path?: never; default: true; }
export type RouteProps<Props> = RoutableProps & { component: AnyComponent<Props> };
export function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;
declare module 'preact' {
namespace JSX {
interface IntrinsicAttributes extends RoutableProps {}
}
interface Attributes extends RoutableProps {}
}

278
node_modules/preact-iso/src/router.js generated vendored Normal file
View File

@@ -0,0 +1,278 @@
import { h, createContext, cloneElement, toChildArray } from 'preact';
import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks';
/**
* @template T
* @typedef {import('preact').RefObject<T>} RefObject
* @typedef {import('./internal.d.ts').VNode} VNode
*/
let push, scope;
const UPDATE = (state, url) => {
push = undefined;
if (url && url.type === 'click') {
// ignore events the browser takes care of already:
if (url.ctrlKey || url.metaKey || url.altKey || url.shiftKey || url.button !== 0) {
return state;
}
const link = url.composedPath().find(el => el.nodeName == 'A' && el.href),
href = link && link.getAttribute('href');
if (
!link ||
link.origin != location.origin ||
/^#/.test(href) ||
!/^(_?self)?$/i.test(link.target) ||
scope && (typeof scope == 'string'
? !href.startsWith(scope)
: !scope.test(href)
)
) {
return state;
}
push = true;
url.preventDefault();
url = link.href.replace(location.origin, '');
} else if (typeof url === 'string') {
push = true;
} else if (url && url.url) {
push = !url.replace;
url = url.url;
} else {
url = location.pathname + location.search;
}
if (push === true) history.pushState(null, '', url);
else if (push === false) history.replaceState(null, '', url);
return url;
};
export const exec = (url, route, matches = {}) => {
url = url.split('/').filter(Boolean);
route = (route || '').split('/').filter(Boolean);
if (!matches.params) matches.params = {};
for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {
let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
val = url[i];
// segment match:
if (!m && param == val) continue;
// /foo/* match
if (!m && val && flag == '*') {
matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/');
break;
}
// segment mismatch / missing required field:
if (!m || (!val && flag != '?' && flag != '*')) return;
rest = flag == '+' || flag == '*';
// rest (+/*) match:
if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined;
// normal/optional field:
else if (val) val = decodeURIComponent(val);
matches.params[param] = val;
if (!(param in matches)) matches[param] = val;
if (rest) break;
}
return matches;
};
/**
* @type {import('./router.d.ts').LocationProvider}
*/
export function LocationProvider(props) {
// @ts-expect-error - props.url is not implemented correctly & will be removed in the future
const [url, route] = useReducer(UPDATE, props.url || location.pathname + location.search);
if (props.scope) scope = props.scope;
const wasPush = push === true;
const value = useMemo(() => {
const u = new URL(url, location.origin);
const path = u.pathname.replace(/\/+$/g, '') || '/';
// @ts-ignore-next
return {
url,
path,
query: Object.fromEntries(u.searchParams),
route: (url, replace) => route({ url, replace }),
wasPush
};
}, [url]);
useLayoutEffect(() => {
addEventListener('click', route);
addEventListener('popstate', route);
return () => {
removeEventListener('click', route);
removeEventListener('popstate', route);
};
}, []);
// @ts-ignore
return h(LocationProvider.ctx.Provider, { value }, props.children);
}
const RESOLVED = Promise.resolve();
/** @this {import('./internal.d.ts').AugmentedComponent} */
export function Router(props) {
const [c, update] = useReducer(c => c + 1, 0);
const { url, query, wasPush, path } = useLocation();
if (!url) {
throw new Error(`preact-iso's <Router> must be used within a <LocationProvider>, see: https://github.com/preactjs/preact-iso#locationprovider`);
}
const { rest = path, params = {} } = useContext(RouteContext);
const isLoading = useRef(false);
const prevRoute = useRef(path);
// Monotonic counter used to check if an un-suspending route is still the current route:
const count = useRef(0);
// The current route:
const cur = /** @type {RefObject<VNode<any>>} */ (useRef());
// Previous route (if current route is suspended):
const prev = /** @type {RefObject<VNode<any>>} */ (useRef());
// A not-yet-hydrated DOM root to remove once we commit:
const pendingBase = /** @type {RefObject<Element | Text>} */ (useRef());
// has this component ever successfully rendered without suspending:
const hasEverCommitted = useRef(false);
// was the most recent render successful (did not suspend):
const didSuspend = /** @type {RefObject<boolean>} */ (useRef());
didSuspend.current = false;
let pathRoute, defaultRoute, matchProps;
toChildArray(props.children).some((/** @type {VNode<any>} */ vnode) => {
const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params, rest: '' }));
if (matches) return (pathRoute = cloneElement(vnode, matchProps));
if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);
});
/** @type {VNode<any> | undefined} */
let incoming = pathRoute || defaultRoute;
const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;
const isHydratingBool = cur.current && cur.current.__h;
const routeChanged = useMemo(() => {
prev.current = cur.current;
cur.current = /** @type {VNode<any>} */ (h(RouteContext.Provider, { value: matchProps }, incoming));
// Only mark as an update if the route component changed.
const outgoing = prev.current && prev.current.props.children;
if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {
// This hack prevents Preact from diffing when we swap `cur` to `prev`:
if (this.__v && this.__v.__k) this.__v.__k.reverse();
count.current++;
return true;
}
return false;
}, [url, JSON.stringify(matchProps)]);
if (isHydratingSuspense) {
cur.current.__u |= MODE_HYDRATE;
cur.current.__u |= MODE_SUSPENDED;
} else if (isHydratingBool) {
cur.current.__h = true;
}
// Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children.
const p = prev.current;
prev.current = null;
// This borrows the _childDidSuspend() solution from compat.
this.__c = (e, suspendedVNode) => {
// Mark the current render as having suspended:
didSuspend.current = true;
// The new route suspended, so keep the previous route around while it loads:
prev.current = p;
// Fire an event saying we're waiting for the route:
if (props.onLoadStart) props.onLoadStart(url);
isLoading.current = true;
// Re-render on unsuspend:
let c = count.current;
e.then(() => {
// Ignore this update if it isn't the most recently suspended update:
if (c !== count.current) return;
// Successful route transition: un-suspend after a tick and stop rendering the old route:
prev.current = null;
if (cur.current) {
if (suspendedVNode.__h) {
// _hydrating
cur.current.__h = suspendedVNode.__h;
}
if (suspendedVNode.__u & MODE_SUSPENDED) {
// _flags
cur.current.__u |= MODE_SUSPENDED;
}
if (suspendedVNode.__u & MODE_HYDRATE) {
cur.current.__u |= MODE_HYDRATE;
}
}
RESOLVED.then(update);
});
};
useLayoutEffect(() => {
const currentDom = this.__v && this.__v.__e;
// Ignore suspended renders (failed commits):
if (didSuspend.current) {
// If we've never committed, mark any hydration DOM for removal on the next commit:
if (!hasEverCommitted.current && !pendingBase.current) {
pendingBase.current = currentDom;
}
return;
}
// If this is the first ever successful commit and we didn't use the hydration DOM, remove it:
if (!hasEverCommitted.current && pendingBase.current) {
if (pendingBase.current !== currentDom) pendingBase.current.remove();
pendingBase.current = null;
}
// Mark the component has having committed:
hasEverCommitted.current = true;
// The route is loaded and rendered.
if (prevRoute.current !== path) {
if (wasPush) scrollTo(0, 0);
if (props.onRouteChange) props.onRouteChange(url);
prevRoute.current = path;
}
if (props.onLoadEnd && isLoading.current) props.onLoadEnd(url);
isLoading.current = false;
}, [path, wasPush, c]);
// Note: cur MUST render first in order to set didSuspend & prev.
return routeChanged
? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })]
: h(RenderRef, { r: cur });
}
const MODE_HYDRATE = 1 << 5;
const MODE_SUSPENDED = 1 << 7;
// Lazily render a ref's current value:
const RenderRef = ({ r }) => r.current;
Router.Provider = LocationProvider;
LocationProvider.ctx = createContext(
/** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({})
);
const RouteContext = createContext(
/** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({})
);
export const Route = props => h(props.component, props);
export const useLocation = () => useContext(LocationProvider.ctx);
export const useRoute = () => useContext(RouteContext);