@native-router/react 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/types/components/Link.d.ts +8 -7
- package/dist/types/components/NavLink.d.ts +8 -22
- package/dist/types/components/PrefetchLink.d.ts +8 -16
- package/dist/types/components/TypedLink.d.ts +7 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/types.d.ts +64 -2
- package/dist/types/use-blocker.d.ts +25 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -72,12 +72,13 @@ function Preview({visible}: {visible: boolean}) {
|
|
|
72
72
|
- Route guards: static `redirect` and async `beforeLoad` on every route level, run shallow → deep; more than 10 chained redirects reject with `RedirectLoopError`
|
|
73
73
|
- Cancelable async navigation: starting a new navigation supersedes the in-flight one; `cancel(router)` aborts it; a history POP cancels it too — and the chain's `AbortSignal` reaches every `data` loader as `ctx.signal` (`fetch(url, {signal: ctx.signal})`), so superseded navigations stop their requests instead of only having results dropped
|
|
74
74
|
- `NavLink` with `isActive`/`isExactActive`, `end`, `caseSensitive` and `aria-current` (defaults to `"page"`); `className`/`style`/`children` accept `({isActive, isExactActive})` callbacks; `to="/"` is active for every path
|
|
75
|
+
- Polymorphic links: every link component takes an `as` component — own props flattened and type-checked on the link, colliding props through the `asProps` escape hatch, `href`/`onClick`/`aria-current` injected, `ref` forwarded
|
|
75
76
|
- `useSearchParams` reads and writes the query string; writes push by default or replace with `{replace: true}`; `useSetSearch(schema)` is the schema-aware setter twin of `useSearch(schema)` — the next value is validated by the same schema before any navigation, a rejection throws `SearchError` without touching the location, and the written query is the schema's own output(defaults applied)
|
|
76
77
|
- Typed search: an optional Standard Schema validator (zod/valibot/arktype, no hard dependency) on any route `search` field, parsed at resolve time — loaders receive a typed `ctx.search` and an invalid search fails the level through the existing error layers; `useSearch(schema?)` reads it in components, degrading to the raw object without a schema
|
|
77
78
|
- Type-safe links: `createRoutes(routes)` checks the table while keeping every `path` literal, `RoutePaths<typeof routes>` extracts the pattern union(through nesting and param segments), and `<TypedLink<RoutePaths<...>> to params>` narrows `to` to the table and checks `params` against the exact pattern's segments — compile errors for unknown paths and missing/wrong params, click-time interpolation with encoding as the runtime backstop
|
|
78
79
|
- `ScrollRestoration` restores the scroll offset per history entry on back/forward and resets it on push (`resetOnPush` to opt out)
|
|
79
80
|
- Router-level `preload(router, to)` shares resolved views across links with in-flight dedup and a 30s TTL; `PrefetchLink` prefetch through it
|
|
80
|
-
- Hooks: `useRouter`, `useView`, `useData<T>(name?)` (typed data of the current level, or named data of ancestor routes), `useMatched` (matched levels, params, location), `useLoading`, `usePrefetch`, `useSearch(schema?)`, `useSetSearch(schema)`
|
|
81
|
+
- Hooks: `useRouter`, `useView`, `useData<T>(name?)` (typed data of the current level, or named data of ancestor routes), `useMatched` (matched levels, params, location), `useLoading`, `usePrefetch`, `useSearch(schema?)`, `useSetSearch(schema)`, `useBlocker(fn)` (unsaved-changes guard: the core `setBlocker` veto, registered while the component is mounted and always asked through the latest closure)
|
|
81
82
|
- Two error layers, both phases: global `errorHandler` prop on the Router, per-route `errorComponent` receiving `{error, ctx}` — `errorComponent` renders for resolve failures(loader/guard/search, no `ctx.phase`) AND for render errors thrown by the component subtree(`ctx.phase === 'render'`, caught by a route-level error boundary so a rendering crash never escapes past its route, like the browser's error page for any failed load)
|
|
82
83
|
- Route-level `pendingComponent` skeleton, shown only when no previous view can be retained (cold start, refresh, re-navigation after an error); the nearest matched ancestor's wins, and in-app navigation keeps the previous view instead
|
|
83
84
|
- Keeping the previous view during in-app navigation is an intentional design following browser-native semantics — see the Design Principles section of the core repository's README
|
|
@@ -102,7 +103,42 @@ function Preview({visible}: {visible: boolean}) {
|
|
|
102
103
|
- `rel` containing `external`
|
|
103
104
|
- events already `defaultPrevented`
|
|
104
105
|
|
|
105
|
-
While a navigation started by a link is pending, further clicks on that link are ignored.
|
|
106
|
+
A user-provided `onClick` runs first with the same event; calling `e.preventDefault()` there suppresses the navigation entirely. While a navigation started by a link is pending, further clicks on that link are ignored.
|
|
107
|
+
|
|
108
|
+
## Rendering through your own component (`as`)
|
|
109
|
+
|
|
110
|
+
`Link`, `NavLink`, `PrefetchLink` and `TypedLink` accept an `as` component: the link renders through it instead of the plain `<a>`, so a design-system link gets SPA navigation, active state and prefetch in one line:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import {NavLink} from '@native-router/react';
|
|
114
|
+
import {NavLink as HazeNavLink} from 'haze-ui';
|
|
115
|
+
|
|
116
|
+
<NavLink as={HazeNavLink} to="/help" variant="primary">
|
|
117
|
+
Help
|
|
118
|
+
</NavLink>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The contract for the `as` component:
|
|
122
|
+
|
|
123
|
+
- **Forward its ref and spread the rest props** onto the DOM element it renders — everything below depends on it (`href`, the composed `onClick` and `aria-current` must reach the DOM).
|
|
124
|
+
- **Own props land directly on the link** (`variant` above): props that do not collide with the link's own props are flattened and checked by TypeScript — required props stay required, invalid values are compile errors.
|
|
125
|
+
- **Colliding props go through `asProps`**: only keys the component shares with the link's base props (anchor attributes such as `title`/`target`) are accepted there, and they are spread last, explicitly overriding the base value. With no shared keys the prop degrades to `{}` — no ambiguity.
|
|
126
|
+
|
|
127
|
+
The navigation semantics stay owned by the link: `href` is always the computed target of `to`, the click handling is always the composed one (user `onClick` → interception guard → in-app navigation) and `NavLink`'s `aria-current` is always its active-state value — none of them can be overridden, not even through `asProps`. The `ref` is the `as` component's own, so a component that is not wrapped in `forwardRef` rejects `ref` at compile time.
|
|
128
|
+
|
|
129
|
+
`TypedLink` composes with `as` on top of its pattern narrowing — give both type arguments, since a partial instantiation does not infer the remaining one:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<TypedLink<RoutePaths<typeof routes>, typeof HazeNavLink>
|
|
133
|
+
to="/users/:id"
|
|
134
|
+
params={{id: '7'}}
|
|
135
|
+
variant="primary"
|
|
136
|
+
>
|
|
137
|
+
User 7
|
|
138
|
+
</TypedLink>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`PrefetchLink`'s strategies keep working through the `as` component; the `viewport` strategy observes the DOM node the component forwards its ref to, so a component that never forwards the ref down to a DOM element simply never triggers a viewport prefetch.
|
|
106
142
|
|
|
107
143
|
## Install
|
|
108
144
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ssr-C-Bgsx5W.cjs");let t=require("react"),r=require("history"),n=require("react/jsx-runtime"),o=require("@native-router/core"),s=require("use-sync-external-store/shim");function
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ssr-C-Bgsx5W.cjs");let t=require("react"),r=require("history"),n=require("react/jsx-runtime"),o=require("@native-router/core"),s=require("use-sync-external-store/shim");function a(e,t,r){return!(e.defaultPrevented||0!==e.button||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||"_blank"===t||"_parent"===t||"_top"===t||(r??"").split(/\s+/).includes("external"))}var c=(0,t.forwardRef)(function({to:r,as:s,asProps:c,onClick:u,...i},l){const f=e.useRouter(),p=(0,t.useRef)(!1),h=s??"a",{"aria-current":d,...y}=i;return(0,n.jsx)(h,{...y,...c,ref:l,href:(0,o.createHref)(f,r),onClick:function(e){u?.(e),a(e,c?.target??i.target,c?.rel??i.rel)&&(e.preventDefault(),p.current||(p.current=!0,(0,o.navigate)(f,r).catch(()=>{}).finally(()=>{p.current=!1})))},"aria-current":d})});c.displayName="Link";var u=c;var i=(0,t.forwardRef)(function({to:r,end:a=!1,caseSensitive:c=!1,className:i,style:l,ariaCurrent:f,children:p,as:h,asProps:d,...y},x){const R=e.useRouter(),v=(0,t.useCallback)(e=>R.history.listen(()=>e()),[R]),m=(0,t.useCallback)(()=>R.history.location.pathname,[R]),w=(0,s.useSyncExternalStore)(v,m,m),k=(0,o.toLocation)(R,r).pathname,[S,C]=c?[w,k]:[w.toLowerCase(),k.toLowerCase()],g=S===C,b=a||g?g:S.startsWith(C.endsWith("/")?C:`${C}/`),P={isActive:b,isExactActive:g};return(0,n.jsx)(u,{to:r,...y,as:h,asProps:d,ref:x,className:"function"==typeof i?i(P):i,style:"function"==typeof l?l(P):l,"aria-current":b?f??"page":void 0,children:"function"==typeof p?p(P):p})});i.displayName="NavLink";var l=(0,t.createContext)({loading:!1});var f=(0,t.forwardRef)(function({to:r,prefetch:s="intent",children:c,as:u,asProps:i,onClick:f,...p},h){const d=e.useRouter(),y=(0,t.useRef)(null),x=(0,t.useRef)(void 0),R=(0,t.useRef)(!1),[v,m]=(0,t.useState)(!1),[w,k]=(0,t.useState)(),[S,C]=(0,t.useState)(),g=(0,t.useCallback)(e=>{y.current=e,"function"==typeof h?h(e):h&&(h.current=e)},[h]);function b(){m(!0),k(void 0),R.current=!1;const e=(0,o.preload)(d,r);return x.current=e,e.then(e=>e.task).then(e=>C(e),e=>{k(e),R.current=!0}).finally(()=>m(!1)),e}function P(){x.current||b()}(0,t.useEffect)(()=>{x.current=void 0,R.current=!1,m(!1),k(void 0),C(void 0)},[r,d]),(0,t.useEffect)(()=>{if("render"===s)return void P();if("viewport"!==s)return;const e=y.current;if(!e||"undefined"==typeof IntersectionObserver)return;const t=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&(t.disconnect(),P())});return t.observe(e),()=>t.disconnect()},[s,r,d]);const L=(0,t.useMemo)(()=>({loading:v,error:w,view:S}),[v,w,S]),j="intent"===s?{onMouseEnter:P,onFocus:P}:void 0,E=u??"a";return(0,n.jsx)(l.Provider,{value:L,children:(0,n.jsx)(E,{...p,...j,...i,ref:g,href:(0,o.createHref)(d,r),onClick:function(e){if(f?.(e),!a(e,i?.target??p.target,i?.rel??p.rel))return;e.preventDefault();const t=x.current;(!t||R.current?b():t).then(e=>(0,o.commit)(d,e.task,e.location)).catch(()=>{}).finally(()=>{x.current=void 0,R.current=!1})},children:c})})});function p(e,t){return e.replace(/\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,(r,n)=>{if(void 0===n)return r;const o=t[n];if(void 0===o||0===o.length)throw new Error(`Missing param "${n}" for the path pattern "${e}"`);return(Array.isArray(o)?o:[o]).map(encodeURIComponent).join("/")})}f.displayName="PrefetchLink";var h=(0,t.forwardRef)(function({to:r,params:s,onClick:c,as:u,asProps:i,...l},f){const h=e.useRouter(),d=(0,t.useRef)(!1);let y=r;try{y=p(r,s??{})}catch{}return(0,n.jsx)(u??"a",{...l,...i,ref:f,href:(0,o.createHref)(h,y),onClick:function(e){if(c?.(e),!a(e,i?.target??l.target,i?.rel??l.rel))return;if(e.preventDefault(),d.current)return;const t=p(r,s??{});d.current=!0,(0,o.navigate)(h,t).catch(()=>{}).finally(()=>{d.current=!1})}})});function d(e){return Object.entries(e).filter(([,e])=>null!=e).map(([e,t])=>(Array.isArray(t)?t:[t]).map(t=>`${encodeURIComponent(e)}=${encodeURIComponent(String(t))}`).join("&")).join("&")}function y(e,t,r){const{history:n}=e,{pathname:s,hash:a}=n.location,c=s+(t?`?${t}`:"")+a;return r?.replace?(0,o.resolveEntry)(e,(0,o.toLocation)(e,c)).then(t=>(0,o.commitReplace)(e,t.task,t.location)):(0,o.navigate)(e,c)}h.displayName="TypedLink",exports.HashRouter=e.HashRouter,exports.HistoryRouter=e.HistoryRouter,exports.Link=c,exports.MemoryRouter=e.MemoryRouter,exports.NavLink=i,exports.PrefetchLink=f,exports.Router=e.Router,exports.ScrollRestoration=function({resetOnPush:n=!0}){const o=e.useRouter(),s=(0,t.useRef)(new Map),a=(0,t.useRef)(-1),c=(0,t.useRef)("");return(0,t.useEffect)(()=>{if("undefined"==typeof window)return;window.history.scrollRestoration&&(window.history.scrollRestoration="manual");const e=s.current,t=e=>e?.index||0;a.current=t(o.history.location.state),c.current=(0,r.createPath)(o.history.location);let u=!1,i=!1,l=!0;const f=o.history.listen(({action:s})=>{i||="POP"===s,u||(u=!0,queueMicrotask(()=>{if(u=!1,!l)return;const s=i;i=!1;const{location:f}=o.history,p=t(f.state),h=(0,r.createPath)(f),d=a.current;if(p!==d&&e.set(d,{x:window.scrollX,y:window.scrollY}),s){const t=e.get(p)??{x:0,y:0};window.scrollTo(t.x,t.y),e.set(p,t)}else!n||p===d&&h===c.current||(window.scrollTo(0,0),e.set(p,{x:0,y:0}));a.current=p,c.current=h}))});return()=>{l=!1,f()}},[o,n]),null},exports.TypedLink=h,exports.View=e.View,exports.createRouter=e.createRouter,exports.createRoutes=function(e){return e},exports.defaultResolveView=e.resolveView,exports.hydrate=e.hydrate,exports.useBlocker=function(r){const n=e.useRouter(),s=(0,t.useRef)(r);s.current=r,(0,t.useEffect)(()=>(0,o.setBlocker)(n,(e,t)=>s.current(e,t)),[n])},exports.useData=e.useData,exports.useLoading=e.useLoading,exports.useMatched=e.useMatched,exports.useNamedData=e.useNamedData,exports.usePrefetch=function(){return(0,t.useContext)(l)},exports.useRouter=e.useRouter,exports.useSearch=function(r){const n=e.useRouter(),a=(0,t.useCallback)(e=>n.history.listen(()=>e()),[n]),c=(0,t.useCallback)(()=>n.history.location.search,[n]),u=(0,t.useCallback)(()=>"",[]),i=(0,s.useSyncExternalStore)(a,c,u);return r?(0,o.parseSearchSync)(r,i):(0,o.parseSearchInput)(i)},exports.useSearchParams=function(){const r=e.useRouter(),n=(0,t.useCallback)(e=>r.history.listen(()=>e()),[r]),o=(0,t.useCallback)(()=>r.history.location.search,[r]),a=(0,t.useCallback)(()=>"",[]);return[new URLSearchParams((0,s.useSyncExternalStore)(n,o,a)),(0,t.useCallback)((e,t)=>{const{history:n}=r,o="function"==typeof e?e(new URLSearchParams(n.location.search)):e;return y(r,o.toString(),t)},[r])]},exports.useSetSearch=function(r){const n=e.useRouter();return(0,t.useCallback)((e,t)=>{const{history:s}=n,a="function"==typeof e?e((0,o.parseSearchInput)(s.location.search)):e,c=(0,o.parseSearchSync)(r,d(a));return y(n,d(c),t)},[n,r])},exports.useView=e.useView;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/components/link-behavior.ts","../src/components/Link.tsx","../src/components/NavLink.tsx","../src/components/PrefetchLink.tsx","../src/components/ScrollRestoration.tsx","../src/components/TypedLink.tsx","../src/create-routes.js","../src/use-search-params.ts"],"sourcesContent":["/**\n * Checks whether a click on an anchor should be intercepted and handled as an\n * in-app navigation, following the standard link interception semantics of\n * react-router/wouter: plain left clicks only.\n *\n * The browser keeps the default behavior whenever this returns `false`, so\n * modified clicks, middle clicks and links to other browsing contexts open in\n * a new tab/window as the user expects.\n *\n * @param e The click event to inspect.\n * @param target The anchor's `target` attribute, if any.\n * @param rel The anchor's `rel` attribute, if any.\n * @group Components\n */\n\nexport function shouldNavigate(\n e: {\n button: number;\n defaultPrevented: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n },\n target?: string,\n rel?: string\n): boolean {\n return (\n !e.defaultPrevented &&\n e.button === 0 &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey &&\n target !== '_blank' &&\n target !== '_parent' &&\n target !== '_top' &&\n !(rel ?? '').split(/\\s+/).includes('external')\n );\n}\n","import {createHref, navigate} from '@native-router/core';\nimport type {LinkProps} from '@@/types';\nimport {useRef, type MouseEvent} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Link for navigate in app.\n * @param props\n * @group Components\n */\nexport default function Link({to, ...rest}: LinkProps) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n\n if (lockRef.current) return;\n lockRef.current = true;\n navigate(router, to)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n return (\n // eslint-disable-next-line jsx-a11y/anchor-has-content\n <a {...rest} href={createHref(router, to)} onClick={handleClick} />\n );\n}\n","import {toLocation} from '@native-router/core';\nimport type {NavLinkProps, NavLinkState} from '@@/types';\nimport {useCallback} from 'react';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './Router';\nimport Link from './Link';\n\n/**\n * Link that knows whether its target matches the current location.\n *\n * Active rules(aligned with react-router's `NavLink`):\n *\n * - `target` is the pathname of `toLocation(router, to)`(baseUrl prepended),\n * `current` is `router.history.location.pathname`; both are lowercased\n * unless `caseSensitive` is set.\n * - `isExactActive`: `current === target`.\n * - `isActive`: `isExactActive` when `end` is set, otherwise `current` equals\n * `target` or starts with `target` plus a trailing `/`(so `to=\"/\"` is active\n * for every path).\n *\n * While active the anchor renders `aria-current={ariaCurrent ?? 'page'}` and\n * `className`/`style`/`children` receive the active state when given as\n * functions. Click behavior is delegated to {@link Link}, inheriting the\n * modified-click guard and the double-click lock.\n * @param props\n * @group Components\n */\nexport default function NavLink({\n to,\n end = false,\n caseSensitive = false,\n className,\n style,\n ariaCurrent,\n children,\n ...rest\n}: NavLinkProps) {\n const router = useRouter();\n // Subscribe to the history location so the active state stays in sync even\n // when rendered outside the routed view(e.g. a nav bar beside <View />).\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n const getSnapshot = useCallback(\n () => router.history.location.pathname,\n [router]\n );\n const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const target = toLocation(router, to).pathname;\n const [currentPath, targetPath] = caseSensitive\n ? [current, target]\n : [current.toLowerCase(), target.toLowerCase()];\n\n const isExactActive = currentPath === targetPath;\n // A root `to=\"/\"` normalizes to the \"/\" prefix and matches every path.\n const isActive =\n end || isExactActive\n ? isExactActive\n : currentPath.startsWith(\n targetPath.endsWith('/') ? targetPath : `${targetPath}/`\n );\n\n const state: NavLinkState = {isActive, isExactActive};\n\n return (\n <Link\n to={to}\n {...rest}\n className={typeof className === 'function' ? className(state) : className}\n style={typeof style === 'function' ? style(state) : style}\n aria-current={isActive ? (ariaCurrent ?? 'page') : undefined}\n >\n {typeof children === 'function' ? children(state) : children}\n </Link>\n );\n}\n","import {commit, createHref, preload} from '@native-router/core';\nimport type {ResolvedEntry} from '@native-router/core';\nimport type {LinkProps, Route} from '@@/types';\nimport {\n createContext,\n MouseEvent,\n ReactNode,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\ntype PrefetchLinkContext = {loading: boolean; error?: Error; view?: ReactNode};\n\nconst Context = createContext<PrefetchLinkContext>({loading: false});\n\n/**\n * Get the prefetch context. Use for render a preview view.\n * @group Hooks\n */\nexport function usePrefetch() {\n return useContext(Context);\n}\n\n/**\n * Link with prefetch support.\n *\n * The `prefetch` prop controls when the target view is resolved:\n * - `'intent'` (default): prefetch on hover or focus.\n * - `'render'`: prefetch as soon as the link mounts.\n * - `'viewport'`: prefetch when the link scrolls into the viewport.\n * - `'none'`: never prefetch; the target is resolved on click.\n *\n * @param props\n * @group Components\n */\nexport default function PrefetchLink({\n to,\n prefetch = 'intent',\n children,\n ...rest\n}: LinkProps) {\n const router = useRouter();\n const anchorRef = useRef<HTMLAnchorElement>(null);\n const entryRef = useRef<Promise<ResolvedEntry<ReactNode>> | undefined>(\n undefined\n );\n const failedRef = useRef(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error>();\n const [view, setView] = useState<ReactNode>();\n\n function prefetchIt(): Promise<ResolvedEntry<ReactNode>> {\n setLoading(true);\n setError(undefined);\n failedRef.current = false;\n // Route guards(redirect/beforeLoad) run before the view resolves; the\n // stored entry carries the terminal location, so prefetch, preview and\n // commit all agree on the final target. preload() caches the entry at\n // the router level(keyed by pathname+search, TTL bounded), so repeated\n // prefetches of the same target share one resolution and the entry is\n // evicted once committed.\n const entryPromise = preload<Route, ReactNode>(router, to);\n entryRef.current = entryPromise;\n // The derived chain carries the loading/error/view state and handles\n // the rejection of the entry task, so a prefetched task that is never\n // committed does not surface as a global unhandledrejection. Failure is\n // tracked in `failedRef` instead of relying on the rejected task itself.\n entryPromise\n .then((entry) => entry.task)\n .then(\n (v) => setView(v),\n (e) => {\n setError(e);\n failedRef.current = true;\n }\n )\n .finally(() => setLoading(false));\n return entryPromise;\n }\n\n function handlePrefetch() {\n if (entryRef.current) return;\n prefetchIt();\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n // A stored entry that already failed can never be committed\n // successfully, so resolve the target again before committing.\n const stored = entryRef.current;\n const entryPromise = !stored || failedRef.current ? prefetchIt() : stored;\n // The terminal location of the entry is committed, not the link target.\n entryPromise\n .then((entry) => commit(router, entry.task, entry.location))\n .catch(() => undefined)\n .finally(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n });\n }\n\n // Reset every piece of prefetch state when the target changes, so a stale\n // error or preview from the previous target is not rendered for the new one.\n useEffect(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n setLoading(false);\n setError(undefined);\n setView(undefined);\n }, [to, router]);\n\n useEffect(() => {\n if (prefetch === 'render') {\n handlePrefetch();\n return undefined;\n }\n if (prefetch !== 'viewport') return undefined;\n const el = anchorRef.current;\n if (!el || typeof IntersectionObserver === 'undefined') return undefined;\n // eslint-disable-next-line compat/compat -- guarded above at runtime\n const observer = new IntersectionObserver((entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return;\n observer.disconnect();\n handlePrefetch();\n });\n observer.observe(el);\n return () => observer.disconnect();\n // `location` and `handlePrefetch` are derived from these deps.\n }, [prefetch, to, router]);\n\n const linkContext = useMemo(\n () => ({loading, error, view}),\n [loading, error, view]\n );\n\n // Hover/focus are intent signals only; 'render', 'viewport' and 'none'\n // never prefetch on interaction.\n const intentHandlers =\n prefetch === 'intent'\n ? {onMouseEnter: handlePrefetch, onFocus: handlePrefetch}\n : undefined;\n\n return (\n <Context.Provider value={linkContext}>\n <a\n {...rest}\n {...intentHandlers}\n ref={anchorRef}\n href={createHref(router, to)}\n onClick={handleClick}\n >\n {children}\n </a>\n </Context.Provider>\n );\n}\n","import {createPath} from 'history';\nimport type {HistoryState} from '@native-router/core';\nimport {useEffect, useRef} from 'react';\nimport {useRouter} from './Router';\n\n/** Saved scroll offset of one history stack slot. */\ntype ScrollPosition = {x: number; y: number};\n\ntype ScrollRestorationProps = {\n /**\n * Scroll a freshly pushed(or replaced) entry back to the top, like a full\n * page load. Set to false to keep the current offset across forward\n * navigations(e.g. feed-style \"load more\" pages). POP always restores the\n * saved offset, regardless of this flag.\n * @default true\n */\n resetOnPush?: boolean;\n};\n\n/**\n * Restore the window scroll position across history navigations.\n *\n * The router restores views from the in-memory `viewStack`: on back/forward\n * the view is reused without re-fetching its data, but the browser has\n * already left the old document position and a remounted DOM starts at the\n * top by default. Mount `<ScrollRestoration />` anywhere inside\n * {@link Router}(typically in the root layout) to fill that gap:\n *\n * - the scroll offset of every visited entry is remembered, keyed by the\n * absolute history index(`history.location.state.index` — the same key\n * `viewStack` is keyed by). Like `viewStack`, the map is in-memory and\n * session scoped: after a page reload there is nothing to restore and the\n * browser's own restoration takes over.\n * - `history.scrollRestoration` is taken over as `manual`(the browser's\n * own `auto` restoration would race the restore and pre-scroll while the\n * left entry's offset is still being read), making the component solely\n * responsible for restoration; the takeover is session scoped and not\n * reverted on unmount, like `viewStack`.\n * - POP restores the saved offset of the landed entry(`0,0` when none was\n * saved, e.g. forward-past-the-end or post-reload entries).\n * - PUSH and REPLACE scroll a fresh entry back to the top when\n * `resetOnPush` is set(default). Internal re-commits of the very same\n * entry(the core listener's POP sync, `refresh`, listen bootstrap) never\n * touch the scroll.\n *\n * Renders nothing. SSR safe: it only subscribes in an effect and only when\n * `window` exists.\n * @param props\n * @group Components\n */\nexport default function ScrollRestoration({\n resetOnPush = true\n}: ScrollRestorationProps) {\n const router = useRouter();\n // Positions survive effect re-runs(resetOnPush changes) on purpose.\n const positionsRef = useRef(new Map<number, ScrollPosition>());\n const lastIndexRef = useRef(-1);\n const lastPathRef = useRef('');\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n // The browser's own `auto` restoration races this component's restore\n // (it can pre-scroll within the microtask window where the left entry's\n // offset is still being read), so take full control for the session.\n if (window.history.scrollRestoration) {\n window.history.scrollRestoration = 'manual';\n }\n\n const positions = positionsRef.current;\n const readIndex = (state: unknown) =>\n (state as HistoryState | undefined)?.index || 0;\n\n lastIndexRef.current = readIndex(router.history.location.state);\n lastPathRef.current = createPath(router.history.location);\n\n let scheduled = false;\n let sawPop = false;\n let active = true;\n\n const unlisten = router.history.listen(({action}) => {\n // The core listener re-commits a landed POP entry with an internal\n // REPLACE(stack serialization sync), and listener registration order\n // decides whether that REPLACE reaches us before or after the POP\n // itself. Collapse the whole synchronous event storm and decide once,\n // from the final history state, in a microtask — no layout has\n // scrolled yet at that point, so saving the left entry's offset\n // still reads the pre-navigation scroll.\n sawPop ||= action === 'POP';\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (!active) return;\n const popped = sawPop;\n sawPop = false;\n const {location} = router.history;\n const index = readIndex(location.state);\n const path = createPath(location);\n const lastIndex = lastIndexRef.current;\n\n if (index !== lastIndex) {\n // The entry we just left keeps its scroll offset.\n positions.set(lastIndex, {x: window.scrollX, y: window.scrollY});\n }\n\n if (popped) {\n const saved = positions.get(index) ?? {x: 0, y: 0};\n window.scrollTo(saved.x, saved.y);\n positions.set(index, saved);\n } else if (\n resetOnPush &&\n (index !== lastIndex || path !== lastPathRef.current)\n ) {\n // A fresh forward entry, or the current entry rewritten to a\n // different location(a replace navigation) — start at the top.\n window.scrollTo(0, 0);\n positions.set(index, {x: 0, y: 0});\n }\n // Anything else is an internal re-commit of the same entry: keep\n // the current scroll.\n\n lastIndexRef.current = index;\n lastPathRef.current = path;\n });\n });\n\n return () => {\n active = false;\n unlisten();\n };\n }, [router, resetOnPush]);\n\n return null;\n}\n","import {createHref, navigate} from '@native-router/core';\nimport {useRef, type MouseEvent, type ReactElement} from 'react';\nimport type {TypedLinkProps} from '@@/types';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Interpolate params into a path pattern: `:name` segments take a\n * string, `*name` wildcards a string array(joined with `/`), both\n * percent-encoded; everything else — including `\\` escapes — is static\n * text. The grammar matches the core matcher's(the ASCII identifier\n * scanner the type-level `ExtractPathParams` models).\n * @throws when a required param is missing or empty — the click-time\n * params check of {@link TypedLink}\n */\nfunction interpolatePath(\n pattern: string,\n params: Record<string, string | string[]>\n): string {\n return pattern.replace(\n /\\\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,\n (match, name?: string) => {\n if (name === undefined) return match;\n const value = params[name];\n if (value === undefined || value.length === 0) {\n throw new Error(\n `Missing param \"${name}\" for the path pattern \"${pattern}\"`\n );\n }\n return (Array.isArray(value) ? value : [value])\n .map(encodeURIComponent)\n .join('/');\n }\n );\n}\n\n/**\n * Link whose `to` is narrowed to a route table's path patterns and whose\n * `params` is checked against the exact pattern's param segments. Give\n * it the pattern union as its type argument:\n *\n * ```tsx\n * const routes = createRoutes({children: [{path: '/users/:id'}, ...]});\n *\n * <TypedLink<RoutePaths<typeof routes>> to=\"/users/:id\" params={{id: '7'}}>\n * User 7\n * </TypedLink>\n * ```\n *\n * A `to` outside the table, a missing required param or a wrong param\n * shape is a compile error; at click time the params are interpolated\n * into the pattern and a missing required param throws instead of\n * navigating(the type-level check's runtime backstop).\n *\n * Without the type argument the component degrades to a plain `Link`:\n * any path, params optional. Click interception follows {@link Link}\n * — only plain primary-button clicks are intercepted.\n * @group Components\n * @param props `to`(a pattern of the table), `params`(per the pattern)\n * and the usual anchor attributes\n */\n// The implementation works on the loose shape; the typed signature is\n// attached below so discriminated-union props need not be destructured\n// across the union.\nfunction TypedLinkImpl({\n to,\n params,\n onClick,\n ...rest\n}: {\n to: string;\n params?: Record<string, string | string[]>;\n} & Omit<TypedLinkProps, 'to' | 'params' | 'prefetch' | 'href'>) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n // The href shows the interpolated target; when a required param is\n // missing the raw pattern stays and the click-time check below blocks\n // the navigation.\n let href: string = to;\n try {\n href = interpolatePath(to, params ?? {});\n } catch {\n // Programming error the types already flag; surfaced on click.\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n\n if (lockRef.current) return;\n // Click-time params check per the pattern's param segments: a missing\n // required param throws instead of navigating.\n const target = interpolatePath(to, params ?? {});\n lockRef.current = true;\n navigate(router, target)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n return (\n // eslint-disable-next-line jsx-a11y/anchor-has-content\n <a {...rest} href={createHref(router, href)} onClick={handleClick} />\n );\n}\n\nconst TypedLink = TypedLinkImpl as <Paths extends string = string>(\n props: TypedLinkProps<Paths>\n) => ReactElement | null;\n\nexport default TypedLink;\n","/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {path: '/', component: () => import('./Home')},\n * {path: '/users/:id', component: () => import('./UserProfile')}\n * ]\n * });\n * // type AppPaths = '/' | '/users/:id'\n * type AppPaths = RoutePaths<typeof routes>;\n * ```\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved\n */\nexport function createRoutes(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead.\n routes\n) {\n return routes;\n}\n","import {useCallback} from 'react';\nimport {\n commitReplace,\n navigate,\n parseSearchInput,\n parseSearchSync,\n resolveEntry,\n toLocation\n} from '@native-router/core';\nimport type {\n SearchInput,\n SearchOutputOf,\n StandardSchemaV1\n} from '@native-router/core';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './components/Router';\n\ntype SetSearchParams = (\n next: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),\n opts?: {replace?: boolean}\n) => Promise<void> | void;\n\n/**\n * Read and write the search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as the Router Component), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; a fresh `URLSearchParams` view is derived from that string\n * on each render.\n *\n * The setter navigates like any other route change: by default the new\n * search is pushed onto the history stack(mainstream react-router\n * semantics), pass `{replace: true}` to rewrite the current entry instead.\n * Since the search is part of the location, every write re-resolves the\n * matched route, so route `data` fetchers(see {@link useData}) observe the\n * new search on the next {@link useData} read.\n *\n * @group Hooks\n * @returns [searchParams, setSearchParams] - the current search params and\n * the setter(functional updates receive the live previous params); the\n * setter returns a `Promise<void>` that resolves once the navigation\n * commits, so callers may optionally `await` it\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a URLSearchParams instance) to keep the\n // snapshot reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n const searchParams = new URLSearchParams(\n useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n );\n\n const setSearchParams = useCallback<SetSearchParams>(\n (next, opts) => {\n const {history} = router;\n const params =\n typeof next === 'function'\n ? // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n next(new URLSearchParams(history.location.search))\n : next;\n return setSearch(router, params.toString(), opts);\n },\n [router]\n );\n\n return [searchParams, setSearchParams];\n}\n\n/**\n * Write the search params of the current location through a schema, the\n * setter-side twin of `useSearch(schema)`: the next value is serialized\n * to a query string, degraded with `parseSearchInput` and validated by\n * the SAME schema before any navigation happens. A schema that rejects\n * the value throws its issues(`SearchError`) without touching the\n * location; the value the schema would default or coerce on the read\n * side never gets silently written.\n *\n * Synchronous schemas only(the `useSearch` flavor); an async `validate`\n * rejects without navigating.\n *\n * The navigation semantics follow `useSearchParams`' setter: push by\n * default, `{replace: true}` rewrites the current entry, guards run,\n * and the returned `Promise<void>` resolves once the navigation commits.\n *\n * @group Hooks\n * @param schema a Standard Schema validator of the search — must\n * validate synchronously\n * @returns the schema-aware setter; functional updates receive the live\n * previous params\n * @throws {SearchError} when `schema` rejects the next value, before\n * any navigation\n */\nexport function useSetSearch<S extends StandardSchemaV1>(\n schema: S\n): (\n next: SearchInput | ((prev: SearchInput) => SearchInput),\n opts?: {replace?: boolean}\n) => Promise<void> | void {\n const router = useRouter();\n\n return useCallback(\n (next, opts) => {\n const {history} = router;\n const input =\n typeof next === 'function'\n ? next(parseSearchInput(history.location.search))\n : next;\n // Validate the whole next value — not a diff — the same way a\n // navigation to the resulting URL would: parseSearchSync throws\n // SearchError with the schema's issues, and no navigation happens.\n const validated = parseSearchSync(schema, stringifySearch(input));\n // The schema output is authoritative: defaults applied and values\n // coerced to strings, so a partially-filled input still writes the\n // fully-defaulted query.\n return setSearch(\n router,\n stringifySearch(validated as Record<string, unknown>),\n opts\n );\n },\n [router, schema]\n );\n}\n\nfunction stringifySearch(input: Record<string, unknown>): string {\n return Object.entries(input)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) =>\n (Array.isArray(value) ? value : [value])\n .map(\n (v) => `${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`\n )\n .join('&')\n )\n .join('&');\n}\n\nfunction setSearch(\n router: ReturnType<typeof useRouter>,\n qs: string,\n opts?: {replace?: boolean}\n): Promise<void> | void {\n const {history} = router;\n const {pathname, hash} = history.location;\n const to = pathname + (qs ? `?${qs}` : '') + hash;\n if (opts?.replace) {\n // Route guards run like any other navigation(align with the push\n // branch): the entry carries the terminal location, so a redirect\n // replaces the current entry with its final target.\n return resolveEntry(router, toLocation(router, to)).then((entry) =>\n commitReplace(router, entry.task, entry.location)\n );\n }\n return navigate(router, to);\n}\n\n/**\n * Read the parsed search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as {@link useSearchParams}), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; the parse itself runs on each render.\n *\n * Without a schema the hook degrades to the raw input object of\n * `parseSearchInput` — strings, arrays for repeated keys\n * (`{page: '2', tag: ['a', 'b']}`). With a schema — any zod/valibot/\n * arktype schema, see the route {@link Route.search search field} — the\n * returned object is the schema's parsed output, so coercion and defaults\n * apply, e.g. `useSearch(pageSchema).page` is a number.\n *\n * Prefer declaring the schema once on the route: its `data` loader then\n * receives a parsed `ctx.search` during resolve, and an invalid search\n * fails the navigation through the existing error channels instead of\n * throwing during render.\n *\n * @group Hooks\n * @param schema an optional Standard Schema validator of the search; it\n * must validate synchronously\n * @returns the parsed search params of the current location\n * @throws {SearchError} when `schema` rejects the current search\n */\nexport function useSearch<S extends StandardSchemaV1>(\n schema: S\n): SearchOutputOf<S>;\nexport function useSearch(): SearchInput;\nexport function useSearch(schema?: StandardSchemaV1): unknown {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a parsed object) to keep the snapshot\n // reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n const search = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getServerSnapshot\n );\n\n return schema ? parseSearchSync(schema, search) : parseSearchInput(search);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/components/link-behavior.ts","../src/components/Link.tsx","../src/components/NavLink.tsx","../src/components/PrefetchLink.tsx","../src/components/ScrollRestoration.tsx","../src/components/TypedLink.tsx","../src/create-routes.js","../src/use-search-params.ts","../src/use-blocker.ts"],"sourcesContent":["/**\n * Checks whether a click on an anchor should be intercepted and handled as an\n * in-app navigation, following the standard link interception semantics of\n * react-router/wouter: plain left clicks only.\n *\n * The browser keeps the default behavior whenever this returns `false`, so\n * modified clicks, middle clicks and links to other browsing contexts open in\n * a new tab/window as the user expects.\n *\n * @param e The click event to inspect.\n * @param target The anchor's `target` attribute, if any.\n * @param rel The anchor's `rel` attribute, if any.\n * @group Components\n */\n\nexport function shouldNavigate(\n e: {\n button: number;\n defaultPrevented: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n },\n target?: string,\n rel?: string\n): boolean {\n return (\n !e.defaultPrevented &&\n e.button === 0 &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey &&\n target !== '_blank' &&\n target !== '_parent' &&\n target !== '_top' &&\n !(rel ?? '').split(/\\s+/).includes('external')\n );\n}\n","import {createHref, navigate} from '@native-router/core';\nimport type {AsLinkProps, LinkProps} from '@@/types';\nimport {\n forwardRef,\n useRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n// The implementation works on the loose shape; the typed signature is\n// attached below so the `as` generic is not threaded through forwardRef's\n// typings(the same pattern TypedLink uses for its discriminated union).\ntype LinkImplProps = LinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n/**\n * Link for navigate in app.\n *\n * Pass an `as` component to render through it instead of the plain anchor:\n * the computed `href` and the navigation-aware click handling are injected,\n * the component's non-conflicting props are accepted directly on the link\n * and its conflicting ones go through `asProps`(see {@link AsLinkProps}).\n * The component should forward its ref and spread the rest props onto the\n * DOM element it renders.\n * @param props\n * @group Components\n */\nfunction LinkImpl(\n {to, as, asProps, onClick, ...rest}: LinkImplProps,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n\n if (lockRef.current) return;\n lockRef.current = true;\n navigate(router, to)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n const A = (as ?? 'a') as ElementType;\n // `aria-current` is NavLink's active-state injection(or a plain anchor\n // attribute on the others): a managed key, so it is injected after\n // asProps like href/onClick instead of traveling through the rest.\n const {'aria-current': ariaCurrent, ...anchorProps} = rest;\n return (\n <A\n {...anchorProps}\n {...asProps}\n ref={ref}\n href={createHref(router, to)}\n onClick={handleClick}\n aria-current={ariaCurrent}\n />\n );\n}\n\n// Two call signatures, generic first: call sites resolve through the `as`\n// generic, while the plain non-generic signature placed LAST is what\n// `ComponentProps<typeof Link>`-style helpers read — without it the\n// uninstantiated generic collapses to its `ElementType` constraint and the\n// ref becomes required `any`. The tail signature is exactly the pre-`as`\n// public shape. The displayName slot keeps the component name settable\n// despite the cast(the forwardRef wrapper shows its own name otherwise).\nconst Link = forwardRef(LinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<LinkProps, A>\n ): ReactElement | null;\n (props: LinkProps): ReactElement | null;\n displayName?: string;\n};\n\nLink.displayName = 'Link';\n\nexport default Link;\n","import {toLocation} from '@native-router/core';\nimport type {AsLinkProps, NavLinkProps, NavLinkState} from '@@/types';\nimport {\n forwardRef,\n useCallback,\n type ElementType,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './Router';\nimport Link from './Link';\n\ntype NavLinkImplProps = NavLinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n// Internal delegation to Link with the implementation-loose `as` shape;\n// the public generic typing lives on both components' public signatures.\nconst LooseLink = Link as (props: any) => ReactElement | null;\n\n/**\n * Link that knows whether its target matches the current location.\n *\n * Active rules(aligned with react-router's `NavLink`):\n *\n * - `target` is the pathname of `toLocation(router, to)`(baseUrl prepended),\n * `current` is `router.history.location.pathname`; both are lowercased\n * unless `caseSensitive` is set.\n * - `isExactActive`: `current === target`.\n * - `isActive`: `isExactActive` when `end` is set, otherwise `current` equals\n * `target` or starts with `target` plus a trailing `/`(so `to=\"/\"` is active\n * for every path).\n *\n * While active the anchor renders `aria-current={ariaCurrent ?? 'page'}` and\n * `className`/`style`/`children` receive the active state when given as\n * functions. Click behavior is delegated to {@link Link}, inheriting the\n * modified-click guard and the double-click lock.\n *\n * Pass an `as` component to render through it instead of the plain anchor\n * (see {@link AsLinkProps}): the active-state callbacks, the computed\n * `className`/`style` and the injected `aria-current` flow to it like any\n * other prop.\n * @param props\n * @group Components\n */\nfunction NavLinkImpl(\n {\n to,\n end = false,\n caseSensitive = false,\n className,\n style,\n ariaCurrent,\n children,\n as,\n asProps,\n ...rest\n }: NavLinkImplProps,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n // Subscribe to the history location so the active state stays in sync even\n // when rendered outside the routed view(e.g. a nav bar beside <View />).\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n const getSnapshot = useCallback(\n () => router.history.location.pathname,\n [router]\n );\n const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const target = toLocation(router, to).pathname;\n const [currentPath, targetPath] = caseSensitive\n ? [current, target]\n : [current.toLowerCase(), target.toLowerCase()];\n\n const isExactActive = currentPath === targetPath;\n // A root `to=\"/\"` normalizes to the \"/\" prefix and matches every path.\n const isActive =\n end || isExactActive\n ? isExactActive\n : currentPath.startsWith(\n targetPath.endsWith('/') ? targetPath : `${targetPath}/`\n );\n\n const state: NavLinkState = {isActive, isExactActive};\n\n return (\n <LooseLink\n to={to}\n {...rest}\n as={as}\n asProps={asProps}\n ref={ref}\n className={typeof className === 'function' ? className(state) : className}\n style={typeof style === 'function' ? style(state) : style}\n aria-current={isActive ? (ariaCurrent ?? 'page') : undefined}\n >\n {typeof children === 'function' ? children(state) : children}\n </LooseLink>\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link).\nconst NavLink = forwardRef(NavLinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<NavLinkProps, A>\n ): ReactElement | null;\n (props: NavLinkProps): ReactElement | null;\n displayName?: string;\n};\n\nNavLink.displayName = 'NavLink';\n\nexport default NavLink;\n","import {commit, createHref, preload} from '@native-router/core';\nimport type {ResolvedEntry} from '@native-router/core';\nimport type {AsLinkProps, LinkProps, Route} from '@@/types';\nimport {\n createContext,\n forwardRef,\n MouseEvent,\n ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ElementType,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\ntype PrefetchLinkContext = {loading: boolean; error?: Error; view?: ReactNode};\n\nconst Context = createContext<PrefetchLinkContext>({loading: false});\n\n/**\n * Get the prefetch context. Use for render a preview view.\n * @group Hooks\n */\nexport function usePrefetch() {\n return useContext(Context);\n}\n\ntype PrefetchLinkImplProps = LinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n/**\n * Link with prefetch support.\n *\n * The `prefetch` prop controls when the target view is resolved:\n * - `'intent'` (default): prefetch on hover or focus.\n * - `'render'`: prefetch as soon as the link mounts.\n * - `'viewport'`: prefetch when the link scrolls into the viewport.\n * - `'none'`: never prefetch; the target is resolved on click.\n *\n * Pass an `as` component to render through it instead of the plain anchor\n * (see {@link AsLinkProps}); every strategy keeps working. The `viewport`\n * strategy observes the DOM node the component forwards its ref to, so a\n * component that does not forward the ref down to a DOM element never\n * triggers a viewport prefetch.\n * @param props\n * @group Components\n */\nfunction PrefetchLinkImpl(\n {\n to,\n prefetch = 'intent',\n children,\n as,\n asProps,\n onClick,\n ...rest\n }: PrefetchLinkImplProps,\n forwardedRef: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const anchorRef = useRef<HTMLAnchorElement>(null);\n const entryRef = useRef<Promise<ResolvedEntry<ReactNode>> | undefined>(\n undefined\n );\n const failedRef = useRef(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error>();\n const [view, setView] = useState<ReactNode>();\n\n // Feed both the internal viewport-observation ref and the user's ref.\n const setAnchorRef = useCallback(\n (node: HTMLAnchorElement | null) => {\n anchorRef.current = node;\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n },\n [forwardedRef]\n );\n\n function prefetchIt(): Promise<ResolvedEntry<ReactNode>> {\n setLoading(true);\n setError(undefined);\n failedRef.current = false;\n // Route guards(redirect/beforeLoad) run before the view resolves; the\n // stored entry carries the terminal location, so prefetch, preview and\n // commit all agree on the final target. preload() caches the entry at\n // the router level(keyed by pathname+search, TTL bounded), so repeated\n // prefetches of the same target share one resolution and the entry is\n // evicted once committed.\n const entryPromise = preload<Route, ReactNode>(router, to);\n entryRef.current = entryPromise;\n // The derived chain carries the loading/error/view state and handles\n // the rejection of the entry task, so a prefetched task that is never\n // committed does not surface as a global unhandledrejection. Failure is\n // tracked in `failedRef` instead of relying on the rejected task itself.\n entryPromise\n .then((entry) => entry.task)\n .then(\n (v) => setView(v),\n (e) => {\n setError(e);\n failedRef.current = true;\n }\n )\n .finally(() => setLoading(false));\n return entryPromise;\n }\n\n function handlePrefetch() {\n if (entryRef.current) return;\n prefetchIt();\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n // A stored entry that already failed can never be committed\n // successfully, so resolve the target again before committing.\n const stored = entryRef.current;\n const entryPromise = !stored || failedRef.current ? prefetchIt() : stored;\n // The terminal location of the entry is committed, not the link target.\n entryPromise\n .then((entry) => commit(router, entry.task, entry.location))\n .catch(() => undefined)\n .finally(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n });\n }\n\n // Reset every piece of prefetch state when the target changes, so a stale\n // error or preview from the previous target is not rendered for the new one.\n useEffect(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n setLoading(false);\n setError(undefined);\n setView(undefined);\n }, [to, router]);\n\n useEffect(() => {\n if (prefetch === 'render') {\n handlePrefetch();\n return undefined;\n }\n if (prefetch !== 'viewport') return undefined;\n const el = anchorRef.current;\n if (!el || typeof IntersectionObserver === 'undefined') return undefined;\n // eslint-disable-next-line compat/compat -- guarded above at runtime\n const observer = new IntersectionObserver((entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return;\n observer.disconnect();\n handlePrefetch();\n });\n observer.observe(el);\n return () => observer.disconnect();\n // `location` and `handlePrefetch` are derived from these deps.\n }, [prefetch, to, router]);\n\n const linkContext = useMemo(\n () => ({loading, error, view}),\n [loading, error, view]\n );\n\n // Hover/focus are intent signals only; 'render', 'viewport' and 'none'\n // never prefetch on interaction.\n const intentHandlers =\n prefetch === 'intent'\n ? {onMouseEnter: handlePrefetch, onFocus: handlePrefetch}\n : undefined;\n\n const A = (as ?? 'a') as ElementType;\n\n return (\n <Context.Provider value={linkContext}>\n <A\n {...rest}\n {...intentHandlers}\n {...asProps}\n ref={setAnchorRef}\n href={createHref(router, to)}\n onClick={handleClick}\n >\n {children}\n </A>\n </Context.Provider>\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link).\nconst PrefetchLink = forwardRef(PrefetchLinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<LinkProps, A>\n ): ReactElement | null;\n (props: LinkProps): ReactElement | null;\n displayName?: string;\n};\n\nPrefetchLink.displayName = 'PrefetchLink';\n\nexport default PrefetchLink;\n","import {createPath} from 'history';\nimport type {HistoryState} from '@native-router/core';\nimport {useEffect, useRef} from 'react';\nimport {useRouter} from './Router';\n\n/** Saved scroll offset of one history stack slot. */\ntype ScrollPosition = {x: number; y: number};\n\ntype ScrollRestorationProps = {\n /**\n * Scroll a freshly pushed(or replaced) entry back to the top, like a full\n * page load. Set to false to keep the current offset across forward\n * navigations(e.g. feed-style \"load more\" pages). POP always restores the\n * saved offset, regardless of this flag.\n * @default true\n */\n resetOnPush?: boolean;\n};\n\n/**\n * Restore the window scroll position across history navigations.\n *\n * The router restores views from the in-memory `viewStack`: on back/forward\n * the view is reused without re-fetching its data, but the browser has\n * already left the old document position and a remounted DOM starts at the\n * top by default. Mount `<ScrollRestoration />` anywhere inside\n * {@link Router}(typically in the root layout) to fill that gap:\n *\n * - the scroll offset of every visited entry is remembered, keyed by the\n * absolute history index(`history.location.state.index` — the same key\n * `viewStack` is keyed by). Like `viewStack`, the map is in-memory and\n * session scoped: after a page reload there is nothing to restore and the\n * browser's own restoration takes over.\n * - `history.scrollRestoration` is taken over as `manual`(the browser's\n * own `auto` restoration would race the restore and pre-scroll while the\n * left entry's offset is still being read), making the component solely\n * responsible for restoration; the takeover is session scoped and not\n * reverted on unmount, like `viewStack`.\n * - POP restores the saved offset of the landed entry(`0,0` when none was\n * saved, e.g. forward-past-the-end or post-reload entries).\n * - PUSH and REPLACE scroll a fresh entry back to the top when\n * `resetOnPush` is set(default). Internal re-commits of the very same\n * entry(the core listener's POP sync, `refresh`, listen bootstrap) never\n * touch the scroll.\n *\n * Renders nothing. SSR safe: it only subscribes in an effect and only when\n * `window` exists.\n * @param props\n * @group Components\n */\nexport default function ScrollRestoration({\n resetOnPush = true\n}: ScrollRestorationProps) {\n const router = useRouter();\n // Positions survive effect re-runs(resetOnPush changes) on purpose.\n const positionsRef = useRef(new Map<number, ScrollPosition>());\n const lastIndexRef = useRef(-1);\n const lastPathRef = useRef('');\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n // The browser's own `auto` restoration races this component's restore\n // (it can pre-scroll within the microtask window where the left entry's\n // offset is still being read), so take full control for the session.\n if (window.history.scrollRestoration) {\n window.history.scrollRestoration = 'manual';\n }\n\n const positions = positionsRef.current;\n const readIndex = (state: unknown) =>\n (state as HistoryState | undefined)?.index || 0;\n\n lastIndexRef.current = readIndex(router.history.location.state);\n lastPathRef.current = createPath(router.history.location);\n\n let scheduled = false;\n let sawPop = false;\n let active = true;\n\n const unlisten = router.history.listen(({action}) => {\n // The core listener re-commits a landed POP entry with an internal\n // REPLACE(stack serialization sync), and listener registration order\n // decides whether that REPLACE reaches us before or after the POP\n // itself. Collapse the whole synchronous event storm and decide once,\n // from the final history state, in a microtask — no layout has\n // scrolled yet at that point, so saving the left entry's offset\n // still reads the pre-navigation scroll.\n sawPop ||= action === 'POP';\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (!active) return;\n const popped = sawPop;\n sawPop = false;\n const {location} = router.history;\n const index = readIndex(location.state);\n const path = createPath(location);\n const lastIndex = lastIndexRef.current;\n\n if (index !== lastIndex) {\n // The entry we just left keeps its scroll offset.\n positions.set(lastIndex, {x: window.scrollX, y: window.scrollY});\n }\n\n if (popped) {\n const saved = positions.get(index) ?? {x: 0, y: 0};\n window.scrollTo(saved.x, saved.y);\n positions.set(index, saved);\n } else if (\n resetOnPush &&\n (index !== lastIndex || path !== lastPathRef.current)\n ) {\n // A fresh forward entry, or the current entry rewritten to a\n // different location(a replace navigation) — start at the top.\n window.scrollTo(0, 0);\n positions.set(index, {x: 0, y: 0});\n }\n // Anything else is an internal re-commit of the same entry: keep\n // the current scroll.\n\n lastIndexRef.current = index;\n lastPathRef.current = path;\n });\n });\n\n return () => {\n active = false;\n unlisten();\n };\n }, [router, resetOnPush]);\n\n return null;\n}\n","import {createHref, navigate} from '@native-router/core';\nimport {\n forwardRef,\n useRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n type Ref\n} from 'react';\nimport type {AsLinkProps, TypedLinkProps} from '@@/types';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Interpolate params into a path pattern: `:name` segments take a\n * string, `*name` wildcards a string array(joined with `/`), both\n * percent-encoded; everything else — including `\\` escapes — is static\n * text. The grammar matches the core matcher's(the ASCII identifier\n * scanner the type-level `ExtractPathParams` models).\n * @throws when a required param is missing or empty — the click-time\n * params check of {@link TypedLink}\n */\nfunction interpolatePath(\n pattern: string,\n params: Record<string, string | string[]>\n): string {\n return pattern.replace(\n /\\\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,\n (match, name?: string) => {\n if (name === undefined) return match;\n const value = params[name];\n if (value === undefined || value.length === 0) {\n throw new Error(\n `Missing param \"${name}\" for the path pattern \"${pattern}\"`\n );\n }\n return (Array.isArray(value) ? value : [value])\n .map(encodeURIComponent)\n .join('/');\n }\n );\n}\n\n/**\n * Link whose `to` is narrowed to a route table's path patterns and whose\n * `params` is checked against the exact pattern's param segments. Give\n * it the pattern union as its type argument:\n *\n * ```tsx\n * const routes = createRoutes({children: [{path: '/users/:id'}, ...]});\n *\n * <TypedLink<RoutePaths<typeof routes>> to=\"/users/:id\" params={{id: '7'}}>\n * User 7\n * </TypedLink>\n * ```\n *\n * A `to` outside the table, a missing required param or a wrong param\n * shape is a compile error; at click time the params are interpolated\n * into the pattern and a missing required param throws instead of\n * navigating(the type-level check's runtime backstop).\n *\n * Without the type argument the component degrades to a plain `Link`:\n * any path, params optional. Click interception follows {@link Link}\n * — only plain primary-button clicks are intercepted.\n *\n * An `as` component can be layered on top(`asProps`/flattened `as`-props\n * per {@link AsLinkProps}); give both type arguments to keep the pattern\n * narrowing: `<TypedLink<Paths, typeof MyLink> ... />`.\n * @group Components\n * @param props `to`(a pattern of the table), `params`(per the pattern)\n * and the usual anchor attributes\n */\n// The implementation works on the loose shape; the typed signature is\n// attached below so discriminated-union props need not be destructured\n// across the union.\nfunction TypedLinkImpl(\n {\n to,\n params,\n onClick,\n as,\n asProps,\n ...rest\n }: {\n to: string;\n params?: Record<string, string | string[]>;\n as?: ElementType;\n asProps?: Record<string, unknown>;\n } & Omit<TypedLinkProps, 'to' | 'params' | 'prefetch' | 'href'>,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n // The href shows the interpolated target; when a required param is\n // missing the raw pattern stays and the click-time check below blocks\n // the navigation.\n let href: string = to;\n try {\n href = interpolatePath(to, params ?? {});\n } catch {\n // Programming error the types already flag; surfaced on click.\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n\n if (lockRef.current) return;\n // Click-time params check per the pattern's param segments: a missing\n // required param throws instead of navigating.\n const target = interpolatePath(to, params ?? {});\n lockRef.current = true;\n navigate(router, target)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n const A = (as ?? 'a') as ElementType;\n return (\n <A\n {...rest}\n {...asProps}\n ref={ref}\n href={createHref(router, href)}\n onClick={handleClick}\n />\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link);\n// the tail keeps the pre-`as` discriminated-union shape.\nconst TypedLink = forwardRef(TypedLinkImpl) as {\n <Paths extends string = string, A extends ElementType = 'a'>(\n props: AsLinkProps<TypedLinkProps<Paths>, A>\n ): ReactElement | null;\n <Paths extends string = string>(\n props: TypedLinkProps<Paths>\n ): ReactElement | null;\n displayName?: string;\n};\n\nTypedLink.displayName = 'TypedLink';\n\nexport default TypedLink;\n","/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {path: '/', component: () => import('./Home')},\n * {path: '/users/:id', component: () => import('./UserProfile')}\n * ]\n * });\n * // type AppPaths = '/' | '/users/:id'\n * type AppPaths = RoutePaths<typeof routes>;\n * ```\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved\n */\nexport function createRoutes(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead.\n routes\n) {\n return routes;\n}\n","import {useCallback} from 'react';\nimport {\n commitReplace,\n navigate,\n parseSearchInput,\n parseSearchSync,\n resolveEntry,\n toLocation\n} from '@native-router/core';\nimport type {\n SearchInput,\n SearchOutputOf,\n StandardSchemaV1\n} from '@native-router/core';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './components/Router';\n\ntype SetSearchParams = (\n next: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),\n opts?: {replace?: boolean}\n) => Promise<void> | void;\n\n/**\n * Read and write the search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as the Router Component), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; a fresh `URLSearchParams` view is derived from that string\n * on each render.\n *\n * The setter navigates like any other route change: by default the new\n * search is pushed onto the history stack(mainstream react-router\n * semantics), pass `{replace: true}` to rewrite the current entry instead.\n * Since the search is part of the location, every write re-resolves the\n * matched route, so route `data` fetchers(see {@link useData}) observe the\n * new search on the next {@link useData} read.\n *\n * @group Hooks\n * @returns [searchParams, setSearchParams] - the current search params and\n * the setter(functional updates receive the live previous params); the\n * setter returns a `Promise<void>` that resolves once the navigation\n * commits, so callers may optionally `await` it\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a URLSearchParams instance) to keep the\n // snapshot reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n const searchParams = new URLSearchParams(\n useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n );\n\n const setSearchParams = useCallback<SetSearchParams>(\n (next, opts) => {\n const {history} = router;\n const params =\n typeof next === 'function'\n ? // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n next(new URLSearchParams(history.location.search))\n : next;\n return setSearch(router, params.toString(), opts);\n },\n [router]\n );\n\n return [searchParams, setSearchParams];\n}\n\n/**\n * Write the search params of the current location through a schema, the\n * setter-side twin of `useSearch(schema)`: the next value is serialized\n * to a query string, degraded with `parseSearchInput` and validated by\n * the SAME schema before any navigation happens. A schema that rejects\n * the value throws its issues(`SearchError`) without touching the\n * location; the value the schema would default or coerce on the read\n * side never gets silently written.\n *\n * Synchronous schemas only(the `useSearch` flavor); an async `validate`\n * rejects without navigating.\n *\n * The navigation semantics follow `useSearchParams`' setter: push by\n * default, `{replace: true}` rewrites the current entry, guards run,\n * and the returned `Promise<void>` resolves once the navigation commits.\n *\n * @group Hooks\n * @param schema a Standard Schema validator of the search — must\n * validate synchronously\n * @returns the schema-aware setter; functional updates receive the live\n * previous params\n * @throws {SearchError} when `schema` rejects the next value, before\n * any navigation\n */\nexport function useSetSearch<S extends StandardSchemaV1>(\n schema: S\n): (\n next: SearchInput | ((prev: SearchInput) => SearchInput),\n opts?: {replace?: boolean}\n) => Promise<void> | void {\n const router = useRouter();\n\n return useCallback(\n (next, opts) => {\n const {history} = router;\n const input =\n typeof next === 'function'\n ? next(parseSearchInput(history.location.search))\n : next;\n // Validate the whole next value — not a diff — the same way a\n // navigation to the resulting URL would: parseSearchSync throws\n // SearchError with the schema's issues, and no navigation happens.\n const validated = parseSearchSync(schema, stringifySearch(input));\n // The schema output is authoritative: defaults applied and values\n // coerced to strings, so a partially-filled input still writes the\n // fully-defaulted query.\n return setSearch(\n router,\n stringifySearch(validated as Record<string, unknown>),\n opts\n );\n },\n [router, schema]\n );\n}\n\nfunction stringifySearch(input: Record<string, unknown>): string {\n return Object.entries(input)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) =>\n (Array.isArray(value) ? value : [value])\n .map(\n (v) => `${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`\n )\n .join('&')\n )\n .join('&');\n}\n\nfunction setSearch(\n router: ReturnType<typeof useRouter>,\n qs: string,\n opts?: {replace?: boolean}\n): Promise<void> | void {\n const {history} = router;\n const {pathname, hash} = history.location;\n const to = pathname + (qs ? `?${qs}` : '') + hash;\n if (opts?.replace) {\n // Route guards run like any other navigation(align with the push\n // branch): the entry carries the terminal location, so a redirect\n // replaces the current entry with its final target.\n return resolveEntry(router, toLocation(router, to)).then((entry) =>\n commitReplace(router, entry.task, entry.location)\n );\n }\n return navigate(router, to);\n}\n\n/**\n * Read the parsed search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as {@link useSearchParams}), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; the parse itself runs on each render.\n *\n * Without a schema the hook degrades to the raw input object of\n * `parseSearchInput` — strings, arrays for repeated keys\n * (`{page: '2', tag: ['a', 'b']}`). With a schema — any zod/valibot/\n * arktype schema, see the route {@link Route.search search field} — the\n * returned object is the schema's parsed output, so coercion and defaults\n * apply, e.g. `useSearch(pageSchema).page` is a number.\n *\n * Prefer declaring the schema once on the route: its `data` loader then\n * receives a parsed `ctx.search` during resolve, and an invalid search\n * fails the navigation through the existing error channels instead of\n * throwing during render.\n *\n * @group Hooks\n * @param schema an optional Standard Schema validator of the search; it\n * must validate synchronously\n * @returns the parsed search params of the current location\n * @throws {SearchError} when `schema` rejects the current search\n */\nexport function useSearch<S extends StandardSchemaV1>(\n schema: S\n): SearchOutputOf<S>;\nexport function useSearch(): SearchInput;\nexport function useSearch(schema?: StandardSchemaV1): unknown {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a parsed object) to keep the snapshot\n // reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n const search = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getServerSnapshot\n );\n\n return schema ? parseSearchSync(schema, search) : parseSearchInput(search);\n}\n","import {useEffect, useRef} from 'react';\nimport {setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn): void {\n const router = useRouter();\n const fnRef = useRef(fn);\n // Always ask the latest closure: re-rendering with new state must not\n // require re-registering the blocker.\n fnRef.current = fn;\n useEffect(\n () => setBlocker(router, (to, from) => fnRef.current(to, from)),\n [router]\n );\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{a as t,c as
|
|
2
|
-
return
|
|
3
|
-
return
|
|
4
|
-
return
|
|
5
|
-
return
|
|
1
|
+
import{a as t,c as r,d as n,f as e,h as o,i,l as c,m as s,o as a,p as u,r as l,s as f,t as h,u as p}from"./ssr-873l9jA-.js";import{createContext as d,forwardRef as y,useCallback as m,useContext as v,useEffect as w,useMemo as g,useRef as k,useState as P}from"react";import{createPath as C}from"history";import{jsx as x}from"react/jsx-runtime";import{commit as A,commitReplace as L,createHref as R,navigate as $,parseSearchInput as N,parseSearchSync as b,preload as j,resolveEntry as I,setBlocker as O,toLocation as S}from"@native-router/core";import{useSyncExternalStore as U}from"use-sync-external-store/shim";function _(t,r,n){return!(t.defaultPrevented||0!==t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||"_blank"===r||"_parent"===r||"_top"===r||(n??"").split(/\s+/).includes("external"))}var K=y(function({to:t,as:n,asProps:e,onClick:o,...i},c){const s=r(),a=k(!1),u=n??"a",{"aria-current":l,...f}=i;/* @__PURE__ */
|
|
2
|
+
return x(u,{...f,...e,ref:c,href:R(s,t),onClick:function(r){o?.(r),_(r,e?.target??i.target,e?.rel??i.rel)&&(r.preventDefault(),a.current||(a.current=!0,$(s,t).catch(()=>{}).finally(()=>{a.current=!1})))},"aria-current":l})});K.displayName="Link";var M=K;var D=y(function({to:t,end:n=!1,caseSensitive:e=!1,className:o,style:i,ariaCurrent:c,children:s,as:a,asProps:u,...l},f){const h=r(),p=m(t=>h.history.listen(()=>t()),[h]),d=m(()=>h.history.location.pathname,[h]),y=U(p,d,d),v=S(h,t).pathname,[w,g]=e?[y,v]:[y.toLowerCase(),v.toLowerCase()],k=w===g,P=n||k?k:w.startsWith(g.endsWith("/")?g:`${g}/`),C={isActive:P,isExactActive:k};/* @__PURE__ */
|
|
3
|
+
return x(M,{to:t,...l,as:a,asProps:u,ref:f,className:"function"==typeof o?o(C):o,style:"function"==typeof i?i(C):i,"aria-current":P?c??"page":void 0,children:"function"==typeof s?s(C):s})});D.displayName="NavLink";var E=d({loading:!1});function T(){return v(E)}var z=y(function({to:t,prefetch:n="intent",children:e,as:o,asProps:i,onClick:c,...s},a){const u=r(),l=k(null),f=k(void 0),h=k(!1),[p,d]=P(!1),[y,v]=P(),[C,L]=P(),$=m(t=>{l.current=t,"function"==typeof a?a(t):a&&(a.current=t)},[a]);function N(){d(!0),v(void 0),h.current=!1;const r=j(u,t);return f.current=r,r.then(t=>t.task).then(t=>L(t),t=>{v(t),h.current=!0}).finally(()=>d(!1)),r}function b(){f.current||N()}w(()=>{f.current=void 0,h.current=!1,d(!1),v(void 0),L(void 0)},[t,u]),w(()=>{if("render"===n)return void b();if("viewport"!==n)return;const t=l.current;if(!t||"undefined"==typeof IntersectionObserver)return;const r=new IntersectionObserver(t=>{t.some(t=>t.isIntersecting)&&(r.disconnect(),b())});return r.observe(t),()=>r.disconnect()},[n,t,u]);const I=g(()=>({loading:p,error:y,view:C}),[p,y,C]),O="intent"===n?{onMouseEnter:b,onFocus:b}:void 0,S=o??"a";/* @__PURE__ */
|
|
4
|
+
return x(E.Provider,{value:I,children:/* @__PURE__ */x(S,{...s,...O,...i,ref:$,href:R(u,t),onClick:function(t){if(c?.(t),!_(t,i?.target??s.target,i?.rel??s.rel))return;t.preventDefault();const r=f.current;(!r||h.current?N():r).then(t=>A(u,t.task,t.location)).catch(()=>{}).finally(()=>{f.current=void 0,h.current=!1})},children:e})})});function W({resetOnPush:t=!0}){const n=r(),e=k(/* @__PURE__ */new Map),o=k(-1),i=k("");return w(()=>{if("undefined"==typeof window)return;window.history.scrollRestoration&&(window.history.scrollRestoration="manual");const r=e.current,c=t=>t?.index||0;o.current=c(n.history.location.state),i.current=C(n.history.location);let s=!1,a=!1,u=!0;const l=n.history.listen(({action:e})=>{a||="POP"===e,s||(s=!0,queueMicrotask(()=>{if(s=!1,!u)return;const e=a;a=!1;const{location:l}=n.history,f=c(l.state),h=C(l),p=o.current;if(f!==p&&r.set(p,{x:window.scrollX,y:window.scrollY}),e){const t=r.get(f)??{x:0,y:0};window.scrollTo(t.x,t.y),r.set(f,t)}else!t||f===p&&h===i.current||(window.scrollTo(0,0),r.set(f,{x:0,y:0}));o.current=f,i.current=h}))});return()=>{u=!1,l()}},[n,t]),null}function Z(t,r){return t.replace(/\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,(n,e)=>{if(void 0===e)return n;const o=r[e];if(void 0===o||0===o.length)throw new Error(`Missing param "${e}" for the path pattern "${t}"`);return(Array.isArray(o)?o:[o]).map(encodeURIComponent).join("/")})}z.displayName="PrefetchLink";var q=y(function({to:t,params:n,onClick:e,as:o,asProps:i,...c},s){const a=r(),u=k(!1);let l=t;try{l=Z(t,n??{})}catch{}/* @__PURE__ */
|
|
5
|
+
return x(o??"a",{...c,...i,ref:s,href:R(a,l),onClick:function(r){if(e?.(r),!_(r,i?.target??c.target,i?.rel??c.rel))return;if(r.preventDefault(),u.current)return;const o=Z(t,n??{});u.current=!0,$(a,o).catch(()=>{}).finally(()=>{u.current=!1})}})});function F(t){return t}function V(){const t=r(),n=m(r=>t.history.listen(()=>r()),[t]),e=m(()=>t.history.location.search,[t]),o=m(()=>"",[]);return[new URLSearchParams(U(n,e,o)),m((r,n)=>{const{history:e}=t,o="function"==typeof r?r(new URLSearchParams(e.location.search)):r;return B(t,o.toString(),n)},[t])]}function X(t){const n=r();return m((r,e)=>{const{history:o}=n,i="function"==typeof r?r(N(o.location.search)):r,c=b(t,Y(i));return B(n,Y(c),e)},[n,t])}function Y(t){return Object.entries(t).filter(([,t])=>null!=t).map(([t,r])=>(Array.isArray(r)?r:[r]).map(r=>`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`).join("&")).join("&")}function B(t,r,n){const{history:e}=t,{pathname:o,hash:i}=e.location,c=o+(r?`?${r}`:"")+i;return n?.replace?I(t,S(t,c)).then(r=>L(t,r.task,r.location)):$(t,c)}function G(t){const n=r(),e=m(t=>n.history.listen(()=>t()),[n]),o=m(()=>n.history.location.search,[n]),i=m(()=>"",[]),c=U(e,o,i);return t?b(t,c):N(c)}function H(t){const n=r(),e=k(t);e.current=t,w(()=>O(n,(t,r)=>e.current(t,r)),[n])}q.displayName="TypedLink";export{l as HashRouter,i as HistoryRouter,K as Link,t as MemoryRouter,D as NavLink,z as PrefetchLink,a as Router,W as ScrollRestoration,q as TypedLink,p as View,f as createRouter,F as createRoutes,c as defaultResolveView,h as hydrate,H as useBlocker,n as useData,e as useLoading,u as useMatched,s as useNamedData,T as usePrefetch,r as useRouter,G as useSearch,V as useSearchParams,X as useSetSearch,o as useView};
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/components/link-behavior.ts","../src/components/Link.tsx","../src/components/NavLink.tsx","../src/components/PrefetchLink.tsx","../src/components/ScrollRestoration.tsx","../src/components/TypedLink.tsx","../src/create-routes.js","../src/use-search-params.ts"],"sourcesContent":["/**\n * Checks whether a click on an anchor should be intercepted and handled as an\n * in-app navigation, following the standard link interception semantics of\n * react-router/wouter: plain left clicks only.\n *\n * The browser keeps the default behavior whenever this returns `false`, so\n * modified clicks, middle clicks and links to other browsing contexts open in\n * a new tab/window as the user expects.\n *\n * @param e The click event to inspect.\n * @param target The anchor's `target` attribute, if any.\n * @param rel The anchor's `rel` attribute, if any.\n * @group Components\n */\n\nexport function shouldNavigate(\n e: {\n button: number;\n defaultPrevented: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n },\n target?: string,\n rel?: string\n): boolean {\n return (\n !e.defaultPrevented &&\n e.button === 0 &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey &&\n target !== '_blank' &&\n target !== '_parent' &&\n target !== '_top' &&\n !(rel ?? '').split(/\\s+/).includes('external')\n );\n}\n","import {createHref, navigate} from '@native-router/core';\nimport type {LinkProps} from '@@/types';\nimport {useRef, type MouseEvent} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Link for navigate in app.\n * @param props\n * @group Components\n */\nexport default function Link({to, ...rest}: LinkProps) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n\n if (lockRef.current) return;\n lockRef.current = true;\n navigate(router, to)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n return (\n // eslint-disable-next-line jsx-a11y/anchor-has-content\n <a {...rest} href={createHref(router, to)} onClick={handleClick} />\n );\n}\n","import {toLocation} from '@native-router/core';\nimport type {NavLinkProps, NavLinkState} from '@@/types';\nimport {useCallback} from 'react';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './Router';\nimport Link from './Link';\n\n/**\n * Link that knows whether its target matches the current location.\n *\n * Active rules(aligned with react-router's `NavLink`):\n *\n * - `target` is the pathname of `toLocation(router, to)`(baseUrl prepended),\n * `current` is `router.history.location.pathname`; both are lowercased\n * unless `caseSensitive` is set.\n * - `isExactActive`: `current === target`.\n * - `isActive`: `isExactActive` when `end` is set, otherwise `current` equals\n * `target` or starts with `target` plus a trailing `/`(so `to=\"/\"` is active\n * for every path).\n *\n * While active the anchor renders `aria-current={ariaCurrent ?? 'page'}` and\n * `className`/`style`/`children` receive the active state when given as\n * functions. Click behavior is delegated to {@link Link}, inheriting the\n * modified-click guard and the double-click lock.\n * @param props\n * @group Components\n */\nexport default function NavLink({\n to,\n end = false,\n caseSensitive = false,\n className,\n style,\n ariaCurrent,\n children,\n ...rest\n}: NavLinkProps) {\n const router = useRouter();\n // Subscribe to the history location so the active state stays in sync even\n // when rendered outside the routed view(e.g. a nav bar beside <View />).\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n const getSnapshot = useCallback(\n () => router.history.location.pathname,\n [router]\n );\n const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const target = toLocation(router, to).pathname;\n const [currentPath, targetPath] = caseSensitive\n ? [current, target]\n : [current.toLowerCase(), target.toLowerCase()];\n\n const isExactActive = currentPath === targetPath;\n // A root `to=\"/\"` normalizes to the \"/\" prefix and matches every path.\n const isActive =\n end || isExactActive\n ? isExactActive\n : currentPath.startsWith(\n targetPath.endsWith('/') ? targetPath : `${targetPath}/`\n );\n\n const state: NavLinkState = {isActive, isExactActive};\n\n return (\n <Link\n to={to}\n {...rest}\n className={typeof className === 'function' ? className(state) : className}\n style={typeof style === 'function' ? style(state) : style}\n aria-current={isActive ? (ariaCurrent ?? 'page') : undefined}\n >\n {typeof children === 'function' ? children(state) : children}\n </Link>\n );\n}\n","import {commit, createHref, preload} from '@native-router/core';\nimport type {ResolvedEntry} from '@native-router/core';\nimport type {LinkProps, Route} from '@@/types';\nimport {\n createContext,\n MouseEvent,\n ReactNode,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\ntype PrefetchLinkContext = {loading: boolean; error?: Error; view?: ReactNode};\n\nconst Context = createContext<PrefetchLinkContext>({loading: false});\n\n/**\n * Get the prefetch context. Use for render a preview view.\n * @group Hooks\n */\nexport function usePrefetch() {\n return useContext(Context);\n}\n\n/**\n * Link with prefetch support.\n *\n * The `prefetch` prop controls when the target view is resolved:\n * - `'intent'` (default): prefetch on hover or focus.\n * - `'render'`: prefetch as soon as the link mounts.\n * - `'viewport'`: prefetch when the link scrolls into the viewport.\n * - `'none'`: never prefetch; the target is resolved on click.\n *\n * @param props\n * @group Components\n */\nexport default function PrefetchLink({\n to,\n prefetch = 'intent',\n children,\n ...rest\n}: LinkProps) {\n const router = useRouter();\n const anchorRef = useRef<HTMLAnchorElement>(null);\n const entryRef = useRef<Promise<ResolvedEntry<ReactNode>> | undefined>(\n undefined\n );\n const failedRef = useRef(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error>();\n const [view, setView] = useState<ReactNode>();\n\n function prefetchIt(): Promise<ResolvedEntry<ReactNode>> {\n setLoading(true);\n setError(undefined);\n failedRef.current = false;\n // Route guards(redirect/beforeLoad) run before the view resolves; the\n // stored entry carries the terminal location, so prefetch, preview and\n // commit all agree on the final target. preload() caches the entry at\n // the router level(keyed by pathname+search, TTL bounded), so repeated\n // prefetches of the same target share one resolution and the entry is\n // evicted once committed.\n const entryPromise = preload<Route, ReactNode>(router, to);\n entryRef.current = entryPromise;\n // The derived chain carries the loading/error/view state and handles\n // the rejection of the entry task, so a prefetched task that is never\n // committed does not surface as a global unhandledrejection. Failure is\n // tracked in `failedRef` instead of relying on the rejected task itself.\n entryPromise\n .then((entry) => entry.task)\n .then(\n (v) => setView(v),\n (e) => {\n setError(e);\n failedRef.current = true;\n }\n )\n .finally(() => setLoading(false));\n return entryPromise;\n }\n\n function handlePrefetch() {\n if (entryRef.current) return;\n prefetchIt();\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n // A stored entry that already failed can never be committed\n // successfully, so resolve the target again before committing.\n const stored = entryRef.current;\n const entryPromise = !stored || failedRef.current ? prefetchIt() : stored;\n // The terminal location of the entry is committed, not the link target.\n entryPromise\n .then((entry) => commit(router, entry.task, entry.location))\n .catch(() => undefined)\n .finally(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n });\n }\n\n // Reset every piece of prefetch state when the target changes, so a stale\n // error or preview from the previous target is not rendered for the new one.\n useEffect(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n setLoading(false);\n setError(undefined);\n setView(undefined);\n }, [to, router]);\n\n useEffect(() => {\n if (prefetch === 'render') {\n handlePrefetch();\n return undefined;\n }\n if (prefetch !== 'viewport') return undefined;\n const el = anchorRef.current;\n if (!el || typeof IntersectionObserver === 'undefined') return undefined;\n // eslint-disable-next-line compat/compat -- guarded above at runtime\n const observer = new IntersectionObserver((entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return;\n observer.disconnect();\n handlePrefetch();\n });\n observer.observe(el);\n return () => observer.disconnect();\n // `location` and `handlePrefetch` are derived from these deps.\n }, [prefetch, to, router]);\n\n const linkContext = useMemo(\n () => ({loading, error, view}),\n [loading, error, view]\n );\n\n // Hover/focus are intent signals only; 'render', 'viewport' and 'none'\n // never prefetch on interaction.\n const intentHandlers =\n prefetch === 'intent'\n ? {onMouseEnter: handlePrefetch, onFocus: handlePrefetch}\n : undefined;\n\n return (\n <Context.Provider value={linkContext}>\n <a\n {...rest}\n {...intentHandlers}\n ref={anchorRef}\n href={createHref(router, to)}\n onClick={handleClick}\n >\n {children}\n </a>\n </Context.Provider>\n );\n}\n","import {createPath} from 'history';\nimport type {HistoryState} from '@native-router/core';\nimport {useEffect, useRef} from 'react';\nimport {useRouter} from './Router';\n\n/** Saved scroll offset of one history stack slot. */\ntype ScrollPosition = {x: number; y: number};\n\ntype ScrollRestorationProps = {\n /**\n * Scroll a freshly pushed(or replaced) entry back to the top, like a full\n * page load. Set to false to keep the current offset across forward\n * navigations(e.g. feed-style \"load more\" pages). POP always restores the\n * saved offset, regardless of this flag.\n * @default true\n */\n resetOnPush?: boolean;\n};\n\n/**\n * Restore the window scroll position across history navigations.\n *\n * The router restores views from the in-memory `viewStack`: on back/forward\n * the view is reused without re-fetching its data, but the browser has\n * already left the old document position and a remounted DOM starts at the\n * top by default. Mount `<ScrollRestoration />` anywhere inside\n * {@link Router}(typically in the root layout) to fill that gap:\n *\n * - the scroll offset of every visited entry is remembered, keyed by the\n * absolute history index(`history.location.state.index` — the same key\n * `viewStack` is keyed by). Like `viewStack`, the map is in-memory and\n * session scoped: after a page reload there is nothing to restore and the\n * browser's own restoration takes over.\n * - `history.scrollRestoration` is taken over as `manual`(the browser's\n * own `auto` restoration would race the restore and pre-scroll while the\n * left entry's offset is still being read), making the component solely\n * responsible for restoration; the takeover is session scoped and not\n * reverted on unmount, like `viewStack`.\n * - POP restores the saved offset of the landed entry(`0,0` when none was\n * saved, e.g. forward-past-the-end or post-reload entries).\n * - PUSH and REPLACE scroll a fresh entry back to the top when\n * `resetOnPush` is set(default). Internal re-commits of the very same\n * entry(the core listener's POP sync, `refresh`, listen bootstrap) never\n * touch the scroll.\n *\n * Renders nothing. SSR safe: it only subscribes in an effect and only when\n * `window` exists.\n * @param props\n * @group Components\n */\nexport default function ScrollRestoration({\n resetOnPush = true\n}: ScrollRestorationProps) {\n const router = useRouter();\n // Positions survive effect re-runs(resetOnPush changes) on purpose.\n const positionsRef = useRef(new Map<number, ScrollPosition>());\n const lastIndexRef = useRef(-1);\n const lastPathRef = useRef('');\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n // The browser's own `auto` restoration races this component's restore\n // (it can pre-scroll within the microtask window where the left entry's\n // offset is still being read), so take full control for the session.\n if (window.history.scrollRestoration) {\n window.history.scrollRestoration = 'manual';\n }\n\n const positions = positionsRef.current;\n const readIndex = (state: unknown) =>\n (state as HistoryState | undefined)?.index || 0;\n\n lastIndexRef.current = readIndex(router.history.location.state);\n lastPathRef.current = createPath(router.history.location);\n\n let scheduled = false;\n let sawPop = false;\n let active = true;\n\n const unlisten = router.history.listen(({action}) => {\n // The core listener re-commits a landed POP entry with an internal\n // REPLACE(stack serialization sync), and listener registration order\n // decides whether that REPLACE reaches us before or after the POP\n // itself. Collapse the whole synchronous event storm and decide once,\n // from the final history state, in a microtask — no layout has\n // scrolled yet at that point, so saving the left entry's offset\n // still reads the pre-navigation scroll.\n sawPop ||= action === 'POP';\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (!active) return;\n const popped = sawPop;\n sawPop = false;\n const {location} = router.history;\n const index = readIndex(location.state);\n const path = createPath(location);\n const lastIndex = lastIndexRef.current;\n\n if (index !== lastIndex) {\n // The entry we just left keeps its scroll offset.\n positions.set(lastIndex, {x: window.scrollX, y: window.scrollY});\n }\n\n if (popped) {\n const saved = positions.get(index) ?? {x: 0, y: 0};\n window.scrollTo(saved.x, saved.y);\n positions.set(index, saved);\n } else if (\n resetOnPush &&\n (index !== lastIndex || path !== lastPathRef.current)\n ) {\n // A fresh forward entry, or the current entry rewritten to a\n // different location(a replace navigation) — start at the top.\n window.scrollTo(0, 0);\n positions.set(index, {x: 0, y: 0});\n }\n // Anything else is an internal re-commit of the same entry: keep\n // the current scroll.\n\n lastIndexRef.current = index;\n lastPathRef.current = path;\n });\n });\n\n return () => {\n active = false;\n unlisten();\n };\n }, [router, resetOnPush]);\n\n return null;\n}\n","import {createHref, navigate} from '@native-router/core';\nimport {useRef, type MouseEvent, type ReactElement} from 'react';\nimport type {TypedLinkProps} from '@@/types';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Interpolate params into a path pattern: `:name` segments take a\n * string, `*name` wildcards a string array(joined with `/`), both\n * percent-encoded; everything else — including `\\` escapes — is static\n * text. The grammar matches the core matcher's(the ASCII identifier\n * scanner the type-level `ExtractPathParams` models).\n * @throws when a required param is missing or empty — the click-time\n * params check of {@link TypedLink}\n */\nfunction interpolatePath(\n pattern: string,\n params: Record<string, string | string[]>\n): string {\n return pattern.replace(\n /\\\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,\n (match, name?: string) => {\n if (name === undefined) return match;\n const value = params[name];\n if (value === undefined || value.length === 0) {\n throw new Error(\n `Missing param \"${name}\" for the path pattern \"${pattern}\"`\n );\n }\n return (Array.isArray(value) ? value : [value])\n .map(encodeURIComponent)\n .join('/');\n }\n );\n}\n\n/**\n * Link whose `to` is narrowed to a route table's path patterns and whose\n * `params` is checked against the exact pattern's param segments. Give\n * it the pattern union as its type argument:\n *\n * ```tsx\n * const routes = createRoutes({children: [{path: '/users/:id'}, ...]});\n *\n * <TypedLink<RoutePaths<typeof routes>> to=\"/users/:id\" params={{id: '7'}}>\n * User 7\n * </TypedLink>\n * ```\n *\n * A `to` outside the table, a missing required param or a wrong param\n * shape is a compile error; at click time the params are interpolated\n * into the pattern and a missing required param throws instead of\n * navigating(the type-level check's runtime backstop).\n *\n * Without the type argument the component degrades to a plain `Link`:\n * any path, params optional. Click interception follows {@link Link}\n * — only plain primary-button clicks are intercepted.\n * @group Components\n * @param props `to`(a pattern of the table), `params`(per the pattern)\n * and the usual anchor attributes\n */\n// The implementation works on the loose shape; the typed signature is\n// attached below so discriminated-union props need not be destructured\n// across the union.\nfunction TypedLinkImpl({\n to,\n params,\n onClick,\n ...rest\n}: {\n to: string;\n params?: Record<string, string | string[]>;\n} & Omit<TypedLinkProps, 'to' | 'params' | 'prefetch' | 'href'>) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n // The href shows the interpolated target; when a required param is\n // missing the raw pattern stays and the click-time check below blocks\n // the navigation.\n let href: string = to;\n try {\n href = interpolatePath(to, params ?? {});\n } catch {\n // Programming error the types already flag; surfaced on click.\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n if (!shouldNavigate(e, rest.target, rest.rel)) return;\n e.preventDefault();\n\n if (lockRef.current) return;\n // Click-time params check per the pattern's param segments: a missing\n // required param throws instead of navigating.\n const target = interpolatePath(to, params ?? {});\n lockRef.current = true;\n navigate(router, target)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n return (\n // eslint-disable-next-line jsx-a11y/anchor-has-content\n <a {...rest} href={createHref(router, href)} onClick={handleClick} />\n );\n}\n\nconst TypedLink = TypedLinkImpl as <Paths extends string = string>(\n props: TypedLinkProps<Paths>\n) => ReactElement | null;\n\nexport default TypedLink;\n","/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {path: '/', component: () => import('./Home')},\n * {path: '/users/:id', component: () => import('./UserProfile')}\n * ]\n * });\n * // type AppPaths = '/' | '/users/:id'\n * type AppPaths = RoutePaths<typeof routes>;\n * ```\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved\n */\nexport function createRoutes(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead.\n routes\n) {\n return routes;\n}\n","import {useCallback} from 'react';\nimport {\n commitReplace,\n navigate,\n parseSearchInput,\n parseSearchSync,\n resolveEntry,\n toLocation\n} from '@native-router/core';\nimport type {\n SearchInput,\n SearchOutputOf,\n StandardSchemaV1\n} from '@native-router/core';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './components/Router';\n\ntype SetSearchParams = (\n next: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),\n opts?: {replace?: boolean}\n) => Promise<void> | void;\n\n/**\n * Read and write the search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as the Router Component), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; a fresh `URLSearchParams` view is derived from that string\n * on each render.\n *\n * The setter navigates like any other route change: by default the new\n * search is pushed onto the history stack(mainstream react-router\n * semantics), pass `{replace: true}` to rewrite the current entry instead.\n * Since the search is part of the location, every write re-resolves the\n * matched route, so route `data` fetchers(see {@link useData}) observe the\n * new search on the next {@link useData} read.\n *\n * @group Hooks\n * @returns [searchParams, setSearchParams] - the current search params and\n * the setter(functional updates receive the live previous params); the\n * setter returns a `Promise<void>` that resolves once the navigation\n * commits, so callers may optionally `await` it\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a URLSearchParams instance) to keep the\n // snapshot reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n const searchParams = new URLSearchParams(\n useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n );\n\n const setSearchParams = useCallback<SetSearchParams>(\n (next, opts) => {\n const {history} = router;\n const params =\n typeof next === 'function'\n ? // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n next(new URLSearchParams(history.location.search))\n : next;\n return setSearch(router, params.toString(), opts);\n },\n [router]\n );\n\n return [searchParams, setSearchParams];\n}\n\n/**\n * Write the search params of the current location through a schema, the\n * setter-side twin of `useSearch(schema)`: the next value is serialized\n * to a query string, degraded with `parseSearchInput` and validated by\n * the SAME schema before any navigation happens. A schema that rejects\n * the value throws its issues(`SearchError`) without touching the\n * location; the value the schema would default or coerce on the read\n * side never gets silently written.\n *\n * Synchronous schemas only(the `useSearch` flavor); an async `validate`\n * rejects without navigating.\n *\n * The navigation semantics follow `useSearchParams`' setter: push by\n * default, `{replace: true}` rewrites the current entry, guards run,\n * and the returned `Promise<void>` resolves once the navigation commits.\n *\n * @group Hooks\n * @param schema a Standard Schema validator of the search — must\n * validate synchronously\n * @returns the schema-aware setter; functional updates receive the live\n * previous params\n * @throws {SearchError} when `schema` rejects the next value, before\n * any navigation\n */\nexport function useSetSearch<S extends StandardSchemaV1>(\n schema: S\n): (\n next: SearchInput | ((prev: SearchInput) => SearchInput),\n opts?: {replace?: boolean}\n) => Promise<void> | void {\n const router = useRouter();\n\n return useCallback(\n (next, opts) => {\n const {history} = router;\n const input =\n typeof next === 'function'\n ? next(parseSearchInput(history.location.search))\n : next;\n // Validate the whole next value — not a diff — the same way a\n // navigation to the resulting URL would: parseSearchSync throws\n // SearchError with the schema's issues, and no navigation happens.\n const validated = parseSearchSync(schema, stringifySearch(input));\n // The schema output is authoritative: defaults applied and values\n // coerced to strings, so a partially-filled input still writes the\n // fully-defaulted query.\n return setSearch(\n router,\n stringifySearch(validated as Record<string, unknown>),\n opts\n );\n },\n [router, schema]\n );\n}\n\nfunction stringifySearch(input: Record<string, unknown>): string {\n return Object.entries(input)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) =>\n (Array.isArray(value) ? value : [value])\n .map(\n (v) => `${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`\n )\n .join('&')\n )\n .join('&');\n}\n\nfunction setSearch(\n router: ReturnType<typeof useRouter>,\n qs: string,\n opts?: {replace?: boolean}\n): Promise<void> | void {\n const {history} = router;\n const {pathname, hash} = history.location;\n const to = pathname + (qs ? `?${qs}` : '') + hash;\n if (opts?.replace) {\n // Route guards run like any other navigation(align with the push\n // branch): the entry carries the terminal location, so a redirect\n // replaces the current entry with its final target.\n return resolveEntry(router, toLocation(router, to)).then((entry) =>\n commitReplace(router, entry.task, entry.location)\n );\n }\n return navigate(router, to);\n}\n\n/**\n * Read the parsed search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as {@link useSearchParams}), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; the parse itself runs on each render.\n *\n * Without a schema the hook degrades to the raw input object of\n * `parseSearchInput` — strings, arrays for repeated keys\n * (`{page: '2', tag: ['a', 'b']}`). With a schema — any zod/valibot/\n * arktype schema, see the route {@link Route.search search field} — the\n * returned object is the schema's parsed output, so coercion and defaults\n * apply, e.g. `useSearch(pageSchema).page` is a number.\n *\n * Prefer declaring the schema once on the route: its `data` loader then\n * receives a parsed `ctx.search` during resolve, and an invalid search\n * fails the navigation through the existing error channels instead of\n * throwing during render.\n *\n * @group Hooks\n * @param schema an optional Standard Schema validator of the search; it\n * must validate synchronously\n * @returns the parsed search params of the current location\n * @throws {SearchError} when `schema` rejects the current search\n */\nexport function useSearch<S extends StandardSchemaV1>(\n schema: S\n): SearchOutputOf<S>;\nexport function useSearch(): SearchInput;\nexport function useSearch(schema?: StandardSchemaV1): unknown {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a parsed object) to keep the snapshot\n // reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n const search = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getServerSnapshot\n );\n\n return schema ? parseSearchSync(schema, search) : parseSearchInput(search);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/components/link-behavior.ts","../src/components/Link.tsx","../src/components/NavLink.tsx","../src/components/PrefetchLink.tsx","../src/components/ScrollRestoration.tsx","../src/components/TypedLink.tsx","../src/create-routes.js","../src/use-search-params.ts","../src/use-blocker.ts"],"sourcesContent":["/**\n * Checks whether a click on an anchor should be intercepted and handled as an\n * in-app navigation, following the standard link interception semantics of\n * react-router/wouter: plain left clicks only.\n *\n * The browser keeps the default behavior whenever this returns `false`, so\n * modified clicks, middle clicks and links to other browsing contexts open in\n * a new tab/window as the user expects.\n *\n * @param e The click event to inspect.\n * @param target The anchor's `target` attribute, if any.\n * @param rel The anchor's `rel` attribute, if any.\n * @group Components\n */\n\nexport function shouldNavigate(\n e: {\n button: number;\n defaultPrevented: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n },\n target?: string,\n rel?: string\n): boolean {\n return (\n !e.defaultPrevented &&\n e.button === 0 &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey &&\n target !== '_blank' &&\n target !== '_parent' &&\n target !== '_top' &&\n !(rel ?? '').split(/\\s+/).includes('external')\n );\n}\n","import {createHref, navigate} from '@native-router/core';\nimport type {AsLinkProps, LinkProps} from '@@/types';\nimport {\n forwardRef,\n useRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n// The implementation works on the loose shape; the typed signature is\n// attached below so the `as` generic is not threaded through forwardRef's\n// typings(the same pattern TypedLink uses for its discriminated union).\ntype LinkImplProps = LinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n/**\n * Link for navigate in app.\n *\n * Pass an `as` component to render through it instead of the plain anchor:\n * the computed `href` and the navigation-aware click handling are injected,\n * the component's non-conflicting props are accepted directly on the link\n * and its conflicting ones go through `asProps`(see {@link AsLinkProps}).\n * The component should forward its ref and spread the rest props onto the\n * DOM element it renders.\n * @param props\n * @group Components\n */\nfunction LinkImpl(\n {to, as, asProps, onClick, ...rest}: LinkImplProps,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n\n if (lockRef.current) return;\n lockRef.current = true;\n navigate(router, to)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n const A = (as ?? 'a') as ElementType;\n // `aria-current` is NavLink's active-state injection(or a plain anchor\n // attribute on the others): a managed key, so it is injected after\n // asProps like href/onClick instead of traveling through the rest.\n const {'aria-current': ariaCurrent, ...anchorProps} = rest;\n return (\n <A\n {...anchorProps}\n {...asProps}\n ref={ref}\n href={createHref(router, to)}\n onClick={handleClick}\n aria-current={ariaCurrent}\n />\n );\n}\n\n// Two call signatures, generic first: call sites resolve through the `as`\n// generic, while the plain non-generic signature placed LAST is what\n// `ComponentProps<typeof Link>`-style helpers read — without it the\n// uninstantiated generic collapses to its `ElementType` constraint and the\n// ref becomes required `any`. The tail signature is exactly the pre-`as`\n// public shape. The displayName slot keeps the component name settable\n// despite the cast(the forwardRef wrapper shows its own name otherwise).\nconst Link = forwardRef(LinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<LinkProps, A>\n ): ReactElement | null;\n (props: LinkProps): ReactElement | null;\n displayName?: string;\n};\n\nLink.displayName = 'Link';\n\nexport default Link;\n","import {toLocation} from '@native-router/core';\nimport type {AsLinkProps, NavLinkProps, NavLinkState} from '@@/types';\nimport {\n forwardRef,\n useCallback,\n type ElementType,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './Router';\nimport Link from './Link';\n\ntype NavLinkImplProps = NavLinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n// Internal delegation to Link with the implementation-loose `as` shape;\n// the public generic typing lives on both components' public signatures.\nconst LooseLink = Link as (props: any) => ReactElement | null;\n\n/**\n * Link that knows whether its target matches the current location.\n *\n * Active rules(aligned with react-router's `NavLink`):\n *\n * - `target` is the pathname of `toLocation(router, to)`(baseUrl prepended),\n * `current` is `router.history.location.pathname`; both are lowercased\n * unless `caseSensitive` is set.\n * - `isExactActive`: `current === target`.\n * - `isActive`: `isExactActive` when `end` is set, otherwise `current` equals\n * `target` or starts with `target` plus a trailing `/`(so `to=\"/\"` is active\n * for every path).\n *\n * While active the anchor renders `aria-current={ariaCurrent ?? 'page'}` and\n * `className`/`style`/`children` receive the active state when given as\n * functions. Click behavior is delegated to {@link Link}, inheriting the\n * modified-click guard and the double-click lock.\n *\n * Pass an `as` component to render through it instead of the plain anchor\n * (see {@link AsLinkProps}): the active-state callbacks, the computed\n * `className`/`style` and the injected `aria-current` flow to it like any\n * other prop.\n * @param props\n * @group Components\n */\nfunction NavLinkImpl(\n {\n to,\n end = false,\n caseSensitive = false,\n className,\n style,\n ariaCurrent,\n children,\n as,\n asProps,\n ...rest\n }: NavLinkImplProps,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n // Subscribe to the history location so the active state stays in sync even\n // when rendered outside the routed view(e.g. a nav bar beside <View />).\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n const getSnapshot = useCallback(\n () => router.history.location.pathname,\n [router]\n );\n const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n const target = toLocation(router, to).pathname;\n const [currentPath, targetPath] = caseSensitive\n ? [current, target]\n : [current.toLowerCase(), target.toLowerCase()];\n\n const isExactActive = currentPath === targetPath;\n // A root `to=\"/\"` normalizes to the \"/\" prefix and matches every path.\n const isActive =\n end || isExactActive\n ? isExactActive\n : currentPath.startsWith(\n targetPath.endsWith('/') ? targetPath : `${targetPath}/`\n );\n\n const state: NavLinkState = {isActive, isExactActive};\n\n return (\n <LooseLink\n to={to}\n {...rest}\n as={as}\n asProps={asProps}\n ref={ref}\n className={typeof className === 'function' ? className(state) : className}\n style={typeof style === 'function' ? style(state) : style}\n aria-current={isActive ? (ariaCurrent ?? 'page') : undefined}\n >\n {typeof children === 'function' ? children(state) : children}\n </LooseLink>\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link).\nconst NavLink = forwardRef(NavLinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<NavLinkProps, A>\n ): ReactElement | null;\n (props: NavLinkProps): ReactElement | null;\n displayName?: string;\n};\n\nNavLink.displayName = 'NavLink';\n\nexport default NavLink;\n","import {commit, createHref, preload} from '@native-router/core';\nimport type {ResolvedEntry} from '@native-router/core';\nimport type {AsLinkProps, LinkProps, Route} from '@@/types';\nimport {\n createContext,\n forwardRef,\n MouseEvent,\n ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ElementType,\n type ReactElement,\n type Ref\n} from 'react';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\ntype PrefetchLinkContext = {loading: boolean; error?: Error; view?: ReactNode};\n\nconst Context = createContext<PrefetchLinkContext>({loading: false});\n\n/**\n * Get the prefetch context. Use for render a preview view.\n * @group Hooks\n */\nexport function usePrefetch() {\n return useContext(Context);\n}\n\ntype PrefetchLinkImplProps = LinkProps & {\n as?: ElementType;\n asProps?: Record<string, unknown>;\n};\n\n/**\n * Link with prefetch support.\n *\n * The `prefetch` prop controls when the target view is resolved:\n * - `'intent'` (default): prefetch on hover or focus.\n * - `'render'`: prefetch as soon as the link mounts.\n * - `'viewport'`: prefetch when the link scrolls into the viewport.\n * - `'none'`: never prefetch; the target is resolved on click.\n *\n * Pass an `as` component to render through it instead of the plain anchor\n * (see {@link AsLinkProps}); every strategy keeps working. The `viewport`\n * strategy observes the DOM node the component forwards its ref to, so a\n * component that does not forward the ref down to a DOM element never\n * triggers a viewport prefetch.\n * @param props\n * @group Components\n */\nfunction PrefetchLinkImpl(\n {\n to,\n prefetch = 'intent',\n children,\n as,\n asProps,\n onClick,\n ...rest\n }: PrefetchLinkImplProps,\n forwardedRef: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const anchorRef = useRef<HTMLAnchorElement>(null);\n const entryRef = useRef<Promise<ResolvedEntry<ReactNode>> | undefined>(\n undefined\n );\n const failedRef = useRef(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error>();\n const [view, setView] = useState<ReactNode>();\n\n // Feed both the internal viewport-observation ref and the user's ref.\n const setAnchorRef = useCallback(\n (node: HTMLAnchorElement | null) => {\n anchorRef.current = node;\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n },\n [forwardedRef]\n );\n\n function prefetchIt(): Promise<ResolvedEntry<ReactNode>> {\n setLoading(true);\n setError(undefined);\n failedRef.current = false;\n // Route guards(redirect/beforeLoad) run before the view resolves; the\n // stored entry carries the terminal location, so prefetch, preview and\n // commit all agree on the final target. preload() caches the entry at\n // the router level(keyed by pathname+search, TTL bounded), so repeated\n // prefetches of the same target share one resolution and the entry is\n // evicted once committed.\n const entryPromise = preload<Route, ReactNode>(router, to);\n entryRef.current = entryPromise;\n // The derived chain carries the loading/error/view state and handles\n // the rejection of the entry task, so a prefetched task that is never\n // committed does not surface as a global unhandledrejection. Failure is\n // tracked in `failedRef` instead of relying on the rejected task itself.\n entryPromise\n .then((entry) => entry.task)\n .then(\n (v) => setView(v),\n (e) => {\n setError(e);\n failedRef.current = true;\n }\n )\n .finally(() => setLoading(false));\n return entryPromise;\n }\n\n function handlePrefetch() {\n if (entryRef.current) return;\n prefetchIt();\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n // A stored entry that already failed can never be committed\n // successfully, so resolve the target again before committing.\n const stored = entryRef.current;\n const entryPromise = !stored || failedRef.current ? prefetchIt() : stored;\n // The terminal location of the entry is committed, not the link target.\n entryPromise\n .then((entry) => commit(router, entry.task, entry.location))\n .catch(() => undefined)\n .finally(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n });\n }\n\n // Reset every piece of prefetch state when the target changes, so a stale\n // error or preview from the previous target is not rendered for the new one.\n useEffect(() => {\n entryRef.current = undefined;\n failedRef.current = false;\n setLoading(false);\n setError(undefined);\n setView(undefined);\n }, [to, router]);\n\n useEffect(() => {\n if (prefetch === 'render') {\n handlePrefetch();\n return undefined;\n }\n if (prefetch !== 'viewport') return undefined;\n const el = anchorRef.current;\n if (!el || typeof IntersectionObserver === 'undefined') return undefined;\n // eslint-disable-next-line compat/compat -- guarded above at runtime\n const observer = new IntersectionObserver((entries) => {\n if (!entries.some((entry) => entry.isIntersecting)) return;\n observer.disconnect();\n handlePrefetch();\n });\n observer.observe(el);\n return () => observer.disconnect();\n // `location` and `handlePrefetch` are derived from these deps.\n }, [prefetch, to, router]);\n\n const linkContext = useMemo(\n () => ({loading, error, view}),\n [loading, error, view]\n );\n\n // Hover/focus are intent signals only; 'render', 'viewport' and 'none'\n // never prefetch on interaction.\n const intentHandlers =\n prefetch === 'intent'\n ? {onMouseEnter: handlePrefetch, onFocus: handlePrefetch}\n : undefined;\n\n const A = (as ?? 'a') as ElementType;\n\n return (\n <Context.Provider value={linkContext}>\n <A\n {...rest}\n {...intentHandlers}\n {...asProps}\n ref={setAnchorRef}\n href={createHref(router, to)}\n onClick={handleClick}\n >\n {children}\n </A>\n </Context.Provider>\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link).\nconst PrefetchLink = forwardRef(PrefetchLinkImpl) as {\n <A extends ElementType = 'a'>(\n props: AsLinkProps<LinkProps, A>\n ): ReactElement | null;\n (props: LinkProps): ReactElement | null;\n displayName?: string;\n};\n\nPrefetchLink.displayName = 'PrefetchLink';\n\nexport default PrefetchLink;\n","import {createPath} from 'history';\nimport type {HistoryState} from '@native-router/core';\nimport {useEffect, useRef} from 'react';\nimport {useRouter} from './Router';\n\n/** Saved scroll offset of one history stack slot. */\ntype ScrollPosition = {x: number; y: number};\n\ntype ScrollRestorationProps = {\n /**\n * Scroll a freshly pushed(or replaced) entry back to the top, like a full\n * page load. Set to false to keep the current offset across forward\n * navigations(e.g. feed-style \"load more\" pages). POP always restores the\n * saved offset, regardless of this flag.\n * @default true\n */\n resetOnPush?: boolean;\n};\n\n/**\n * Restore the window scroll position across history navigations.\n *\n * The router restores views from the in-memory `viewStack`: on back/forward\n * the view is reused without re-fetching its data, but the browser has\n * already left the old document position and a remounted DOM starts at the\n * top by default. Mount `<ScrollRestoration />` anywhere inside\n * {@link Router}(typically in the root layout) to fill that gap:\n *\n * - the scroll offset of every visited entry is remembered, keyed by the\n * absolute history index(`history.location.state.index` — the same key\n * `viewStack` is keyed by). Like `viewStack`, the map is in-memory and\n * session scoped: after a page reload there is nothing to restore and the\n * browser's own restoration takes over.\n * - `history.scrollRestoration` is taken over as `manual`(the browser's\n * own `auto` restoration would race the restore and pre-scroll while the\n * left entry's offset is still being read), making the component solely\n * responsible for restoration; the takeover is session scoped and not\n * reverted on unmount, like `viewStack`.\n * - POP restores the saved offset of the landed entry(`0,0` when none was\n * saved, e.g. forward-past-the-end or post-reload entries).\n * - PUSH and REPLACE scroll a fresh entry back to the top when\n * `resetOnPush` is set(default). Internal re-commits of the very same\n * entry(the core listener's POP sync, `refresh`, listen bootstrap) never\n * touch the scroll.\n *\n * Renders nothing. SSR safe: it only subscribes in an effect and only when\n * `window` exists.\n * @param props\n * @group Components\n */\nexport default function ScrollRestoration({\n resetOnPush = true\n}: ScrollRestorationProps) {\n const router = useRouter();\n // Positions survive effect re-runs(resetOnPush changes) on purpose.\n const positionsRef = useRef(new Map<number, ScrollPosition>());\n const lastIndexRef = useRef(-1);\n const lastPathRef = useRef('');\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined;\n\n // The browser's own `auto` restoration races this component's restore\n // (it can pre-scroll within the microtask window where the left entry's\n // offset is still being read), so take full control for the session.\n if (window.history.scrollRestoration) {\n window.history.scrollRestoration = 'manual';\n }\n\n const positions = positionsRef.current;\n const readIndex = (state: unknown) =>\n (state as HistoryState | undefined)?.index || 0;\n\n lastIndexRef.current = readIndex(router.history.location.state);\n lastPathRef.current = createPath(router.history.location);\n\n let scheduled = false;\n let sawPop = false;\n let active = true;\n\n const unlisten = router.history.listen(({action}) => {\n // The core listener re-commits a landed POP entry with an internal\n // REPLACE(stack serialization sync), and listener registration order\n // decides whether that REPLACE reaches us before or after the POP\n // itself. Collapse the whole synchronous event storm and decide once,\n // from the final history state, in a microtask — no layout has\n // scrolled yet at that point, so saving the left entry's offset\n // still reads the pre-navigation scroll.\n sawPop ||= action === 'POP';\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (!active) return;\n const popped = sawPop;\n sawPop = false;\n const {location} = router.history;\n const index = readIndex(location.state);\n const path = createPath(location);\n const lastIndex = lastIndexRef.current;\n\n if (index !== lastIndex) {\n // The entry we just left keeps its scroll offset.\n positions.set(lastIndex, {x: window.scrollX, y: window.scrollY});\n }\n\n if (popped) {\n const saved = positions.get(index) ?? {x: 0, y: 0};\n window.scrollTo(saved.x, saved.y);\n positions.set(index, saved);\n } else if (\n resetOnPush &&\n (index !== lastIndex || path !== lastPathRef.current)\n ) {\n // A fresh forward entry, or the current entry rewritten to a\n // different location(a replace navigation) — start at the top.\n window.scrollTo(0, 0);\n positions.set(index, {x: 0, y: 0});\n }\n // Anything else is an internal re-commit of the same entry: keep\n // the current scroll.\n\n lastIndexRef.current = index;\n lastPathRef.current = path;\n });\n });\n\n return () => {\n active = false;\n unlisten();\n };\n }, [router, resetOnPush]);\n\n return null;\n}\n","import {createHref, navigate} from '@native-router/core';\nimport {\n forwardRef,\n useRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n type Ref\n} from 'react';\nimport type {AsLinkProps, TypedLinkProps} from '@@/types';\nimport {useRouter} from './Router';\nimport {shouldNavigate} from './link-behavior';\n\n/**\n * Interpolate params into a path pattern: `:name` segments take a\n * string, `*name` wildcards a string array(joined with `/`), both\n * percent-encoded; everything else — including `\\` escapes — is static\n * text. The grammar matches the core matcher's(the ASCII identifier\n * scanner the type-level `ExtractPathParams` models).\n * @throws when a required param is missing or empty — the click-time\n * params check of {@link TypedLink}\n */\nfunction interpolatePath(\n pattern: string,\n params: Record<string, string | string[]>\n): string {\n return pattern.replace(\n /\\\\.|[:*]([A-Za-z_$][A-Za-z0-9_$]*)/g,\n (match, name?: string) => {\n if (name === undefined) return match;\n const value = params[name];\n if (value === undefined || value.length === 0) {\n throw new Error(\n `Missing param \"${name}\" for the path pattern \"${pattern}\"`\n );\n }\n return (Array.isArray(value) ? value : [value])\n .map(encodeURIComponent)\n .join('/');\n }\n );\n}\n\n/**\n * Link whose `to` is narrowed to a route table's path patterns and whose\n * `params` is checked against the exact pattern's param segments. Give\n * it the pattern union as its type argument:\n *\n * ```tsx\n * const routes = createRoutes({children: [{path: '/users/:id'}, ...]});\n *\n * <TypedLink<RoutePaths<typeof routes>> to=\"/users/:id\" params={{id: '7'}}>\n * User 7\n * </TypedLink>\n * ```\n *\n * A `to` outside the table, a missing required param or a wrong param\n * shape is a compile error; at click time the params are interpolated\n * into the pattern and a missing required param throws instead of\n * navigating(the type-level check's runtime backstop).\n *\n * Without the type argument the component degrades to a plain `Link`:\n * any path, params optional. Click interception follows {@link Link}\n * — only plain primary-button clicks are intercepted.\n *\n * An `as` component can be layered on top(`asProps`/flattened `as`-props\n * per {@link AsLinkProps}); give both type arguments to keep the pattern\n * narrowing: `<TypedLink<Paths, typeof MyLink> ... />`.\n * @group Components\n * @param props `to`(a pattern of the table), `params`(per the pattern)\n * and the usual anchor attributes\n */\n// The implementation works on the loose shape; the typed signature is\n// attached below so discriminated-union props need not be destructured\n// across the union.\nfunction TypedLinkImpl(\n {\n to,\n params,\n onClick,\n as,\n asProps,\n ...rest\n }: {\n to: string;\n params?: Record<string, string | string[]>;\n as?: ElementType;\n asProps?: Record<string, unknown>;\n } & Omit<TypedLinkProps, 'to' | 'params' | 'prefetch' | 'href'>,\n ref: Ref<HTMLAnchorElement>\n) {\n const router = useRouter();\n const lockRef = useRef(false);\n\n // The href shows the interpolated target; when a required param is\n // missing the raw pattern stays and the click-time check below blocks\n // the navigation.\n let href: string = to;\n try {\n href = interpolatePath(to, params ?? {});\n } catch {\n // Programming error the types already flag; surfaced on click.\n }\n\n function handleClick(e: MouseEvent<HTMLAnchorElement>) {\n // The user's onClick runs first with the same event; calling\n // e.preventDefault() there suppresses the navigation entirely.\n onClick?.(e);\n // Modified clicks, other buttons and links to another browsing context\n // keep the browser default behavior (open in new tab/window, etc).\n // asProps overrides the base anchor attributes at runtime, so the guard\n // judges the effective target/rel — the ones actually rendered.\n if (\n !shouldNavigate(\n e,\n (asProps?.target as string | undefined) ?? rest.target,\n (asProps?.rel as string | undefined) ?? rest.rel\n )\n )\n return;\n e.preventDefault();\n\n if (lockRef.current) return;\n // Click-time params check per the pattern's param segments: a missing\n // required param throws instead of navigating.\n const target = interpolatePath(to, params ?? {});\n lockRef.current = true;\n navigate(router, target)\n .catch(() => undefined)\n .finally(() => {\n lockRef.current = false;\n });\n }\n\n const A = (as ?? 'a') as ElementType;\n return (\n <A\n {...rest}\n {...asProps}\n ref={ref}\n href={createHref(router, href)}\n onClick={handleClick}\n />\n );\n}\n\n// Generic first for call sites, plain tail for ComponentProps(see Link);\n// the tail keeps the pre-`as` discriminated-union shape.\nconst TypedLink = forwardRef(TypedLinkImpl) as {\n <Paths extends string = string, A extends ElementType = 'a'>(\n props: AsLinkProps<TypedLinkProps<Paths>, A>\n ): ReactElement | null;\n <Paths extends string = string>(\n props: TypedLinkProps<Paths>\n ): ReactElement | null;\n displayName?: string;\n};\n\nTypedLink.displayName = 'TypedLink';\n\nexport default TypedLink;\n","/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {path: '/', component: () => import('./Home')},\n * {path: '/users/:id', component: () => import('./UserProfile')}\n * ]\n * });\n * // type AppPaths = '/' | '/users/:id'\n * type AppPaths = RoutePaths<typeof routes>;\n * ```\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved\n */\nexport function createRoutes(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead.\n routes\n) {\n return routes;\n}\n","import {useCallback} from 'react';\nimport {\n commitReplace,\n navigate,\n parseSearchInput,\n parseSearchSync,\n resolveEntry,\n toLocation\n} from '@native-router/core';\nimport type {\n SearchInput,\n SearchOutputOf,\n StandardSchemaV1\n} from '@native-router/core';\nimport {useSyncExternalStore} from 'use-sync-external-store/shim';\nimport {useRouter} from './components/Router';\n\ntype SetSearchParams = (\n next: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),\n opts?: {replace?: boolean}\n) => Promise<void> | void;\n\n/**\n * Read and write the search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as the Router Component), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; a fresh `URLSearchParams` view is derived from that string\n * on each render.\n *\n * The setter navigates like any other route change: by default the new\n * search is pushed onto the history stack(mainstream react-router\n * semantics), pass `{replace: true}` to rewrite the current entry instead.\n * Since the search is part of the location, every write re-resolves the\n * matched route, so route `data` fetchers(see {@link useData}) observe the\n * new search on the next {@link useData} read.\n *\n * @group Hooks\n * @returns [searchParams, setSearchParams] - the current search params and\n * the setter(functional updates receive the live previous params); the\n * setter returns a `Promise<void>` that resolves once the navigation\n * commits, so callers may optionally `await` it\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a URLSearchParams instance) to keep the\n // snapshot reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n const searchParams = new URLSearchParams(\n useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n );\n\n const setSearchParams = useCallback<SetSearchParams>(\n (next, opts) => {\n const {history} = router;\n const params =\n typeof next === 'function'\n ? // eslint-disable-next-line compat/compat -- URLSearchParams support is the app's polyfill concern, not bundled\n next(new URLSearchParams(history.location.search))\n : next;\n return setSearch(router, params.toString(), opts);\n },\n [router]\n );\n\n return [searchParams, setSearchParams];\n}\n\n/**\n * Write the search params of the current location through a schema, the\n * setter-side twin of `useSearch(schema)`: the next value is serialized\n * to a query string, degraded with `parseSearchInput` and validated by\n * the SAME schema before any navigation happens. A schema that rejects\n * the value throws its issues(`SearchError`) without touching the\n * location; the value the schema would default or coerce on the read\n * side never gets silently written.\n *\n * Synchronous schemas only(the `useSearch` flavor); an async `validate`\n * rejects without navigating.\n *\n * The navigation semantics follow `useSearchParams`' setter: push by\n * default, `{replace: true}` rewrites the current entry, guards run,\n * and the returned `Promise<void>` resolves once the navigation commits.\n *\n * @group Hooks\n * @param schema a Standard Schema validator of the search — must\n * validate synchronously\n * @returns the schema-aware setter; functional updates receive the live\n * previous params\n * @throws {SearchError} when `schema` rejects the next value, before\n * any navigation\n */\nexport function useSetSearch<S extends StandardSchemaV1>(\n schema: S\n): (\n next: SearchInput | ((prev: SearchInput) => SearchInput),\n opts?: {replace?: boolean}\n) => Promise<void> | void {\n const router = useRouter();\n\n return useCallback(\n (next, opts) => {\n const {history} = router;\n const input =\n typeof next === 'function'\n ? next(parseSearchInput(history.location.search))\n : next;\n // Validate the whole next value — not a diff — the same way a\n // navigation to the resulting URL would: parseSearchSync throws\n // SearchError with the schema's issues, and no navigation happens.\n const validated = parseSearchSync(schema, stringifySearch(input));\n // The schema output is authoritative: defaults applied and values\n // coerced to strings, so a partially-filled input still writes the\n // fully-defaulted query.\n return setSearch(\n router,\n stringifySearch(validated as Record<string, unknown>),\n opts\n );\n },\n [router, schema]\n );\n}\n\nfunction stringifySearch(input: Record<string, unknown>): string {\n return Object.entries(input)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) =>\n (Array.isArray(value) ? value : [value])\n .map(\n (v) => `${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`\n )\n .join('&')\n )\n .join('&');\n}\n\nfunction setSearch(\n router: ReturnType<typeof useRouter>,\n qs: string,\n opts?: {replace?: boolean}\n): Promise<void> | void {\n const {history} = router;\n const {pathname, hash} = history.location;\n const to = pathname + (qs ? `?${qs}` : '') + hash;\n if (opts?.replace) {\n // Route guards run like any other navigation(align with the push\n // branch): the entry carries the terminal location, so a redirect\n // replaces the current entry with its final target.\n return resolveEntry(router, toLocation(router, to)).then((entry) =>\n commitReplace(router, entry.task, entry.location)\n );\n }\n return navigate(router, to);\n}\n\n/**\n * Read the parsed search params of the current location.\n *\n * The raw `location.search` string is subscribed via\n * `useSyncExternalStore`(same source as {@link useSearchParams}), so every\n * location change(push, replace or pop) re-renders the component with the\n * latest params; the parse itself runs on each render.\n *\n * Without a schema the hook degrades to the raw input object of\n * `parseSearchInput` — strings, arrays for repeated keys\n * (`{page: '2', tag: ['a', 'b']}`). With a schema — any zod/valibot/\n * arktype schema, see the route {@link Route.search search field} — the\n * returned object is the schema's parsed output, so coercion and defaults\n * apply, e.g. `useSearch(pageSchema).page` is a number.\n *\n * Prefer declaring the schema once on the route: its `data` loader then\n * receives a parsed `ctx.search` during resolve, and an invalid search\n * fails the navigation through the existing error channels instead of\n * throwing during render.\n *\n * @group Hooks\n * @param schema an optional Standard Schema validator of the search; it\n * must validate synchronously\n * @returns the parsed search params of the current location\n * @throws {SearchError} when `schema` rejects the current search\n */\nexport function useSearch<S extends StandardSchemaV1>(\n schema: S\n): SearchOutputOf<S>;\nexport function useSearch(): SearchInput;\nexport function useSearch(schema?: StandardSchemaV1): unknown {\n const router = useRouter();\n\n // Subscribe to the raw history(not the core view listener): search must\n // update on ANY location change, including same-view re-resolves.\n const subscribe = useCallback(\n (onStoreChange: () => void) => router.history.listen(() => onStoreChange()),\n [router]\n );\n // Snapshot the raw string(never a parsed object) to keep the snapshot\n // reference stable between renders.\n const getSnapshot = useCallback(\n () => router.history.location.search,\n [router]\n );\n // There is no meaningful search during SSR.\n const getServerSnapshot = useCallback(() => '', []);\n\n const search = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getServerSnapshot\n );\n\n return schema ? parseSearchSync(schema, search) : parseSearchInput(search);\n}\n","import {useEffect, useRef} from 'react';\nimport {setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn): void {\n const router = useRouter();\n const fnRef = useRef(fn);\n // Always ask the latest closure: re-rendering with new state must not\n // require re-registering the blocker.\n fnRef.current = fn;\n useEffect(\n () => setBlocker(router, (to, from) => fnRef.current(to, from)),\n [router]\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { LinkProps } from '../types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import type { AsLinkProps, LinkProps } from '../types';
|
|
2
|
+
import { type ElementType, type ReactElement } from 'react';
|
|
3
|
+
declare const Link: {
|
|
4
|
+
<A extends ElementType = "a">(props: AsLinkProps<LinkProps, A>): ReactElement | null;
|
|
5
|
+
(props: LinkProps): ReactElement | null;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
};
|
|
8
|
+
export default Link;
|
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import type { NavLinkProps } from '../types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* unless `caseSensitive` is set.
|
|
10
|
-
* - `isExactActive`: `current === target`.
|
|
11
|
-
* - `isActive`: `isExactActive` when `end` is set, otherwise `current` equals
|
|
12
|
-
* `target` or starts with `target` plus a trailing `/`(so `to="/"` is active
|
|
13
|
-
* for every path).
|
|
14
|
-
*
|
|
15
|
-
* While active the anchor renders `aria-current={ariaCurrent ?? 'page'}` and
|
|
16
|
-
* `className`/`style`/`children` receive the active state when given as
|
|
17
|
-
* functions. Click behavior is delegated to {@link Link}, inheriting the
|
|
18
|
-
* modified-click guard and the double-click lock.
|
|
19
|
-
* @param props
|
|
20
|
-
* @group Components
|
|
21
|
-
*/
|
|
22
|
-
export default function NavLink({ to, end, caseSensitive, className, style, ariaCurrent, children, ...rest }: NavLinkProps): import("react").JSX.Element;
|
|
1
|
+
import type { AsLinkProps, NavLinkProps } from '../types';
|
|
2
|
+
import { type ElementType, type ReactElement } from 'react';
|
|
3
|
+
declare const NavLink: {
|
|
4
|
+
<A extends ElementType = "a">(props: AsLinkProps<NavLinkProps, A>): ReactElement | null;
|
|
5
|
+
(props: NavLinkProps): ReactElement | null;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
};
|
|
8
|
+
export default NavLink;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LinkProps } from '../types';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
1
|
+
import type { AsLinkProps, LinkProps } from '../types';
|
|
2
|
+
import { ReactNode, type ElementType, type ReactElement } from 'react';
|
|
3
3
|
type PrefetchLinkContext = {
|
|
4
4
|
loading: boolean;
|
|
5
5
|
error?: Error;
|
|
@@ -10,17 +10,9 @@ type PrefetchLinkContext = {
|
|
|
10
10
|
* @group Hooks
|
|
11
11
|
*/
|
|
12
12
|
export declare function usePrefetch(): PrefetchLinkContext;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* - `'viewport'`: prefetch when the link scrolls into the viewport.
|
|
20
|
-
* - `'none'`: never prefetch; the target is resolved on click.
|
|
21
|
-
*
|
|
22
|
-
* @param props
|
|
23
|
-
* @group Components
|
|
24
|
-
*/
|
|
25
|
-
export default function PrefetchLink({ to, prefetch, children, ...rest }: LinkProps): import("react").JSX.Element;
|
|
26
|
-
export {};
|
|
13
|
+
declare const PrefetchLink: {
|
|
14
|
+
<A extends ElementType = "a">(props: AsLinkProps<LinkProps, A>): ReactElement | null;
|
|
15
|
+
(props: LinkProps): ReactElement | null;
|
|
16
|
+
displayName?: string;
|
|
17
|
+
};
|
|
18
|
+
export default PrefetchLink;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { type ReactElement } from 'react';
|
|
2
|
-
import type { TypedLinkProps } from '../types';
|
|
3
|
-
declare const TypedLink:
|
|
1
|
+
import { type ElementType, type ReactElement } from 'react';
|
|
2
|
+
import type { AsLinkProps, TypedLinkProps } from '../types';
|
|
3
|
+
declare const TypedLink: {
|
|
4
|
+
<Paths extends string = string, A extends ElementType = "a">(props: AsLinkProps<TypedLinkProps<Paths>, A>): ReactElement | null;
|
|
5
|
+
<Paths extends string = string>(props: TypedLinkProps<Paths>): ReactElement | null;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
};
|
|
4
8
|
export default TypedLink;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as TypedLink } from './components/TypedLink';
|
|
|
7
7
|
export { createRoutes } from './create-routes';
|
|
8
8
|
export { useView, View, useData, useNamedData, useLoading, useMatched } from './context';
|
|
9
9
|
export { useSearchParams, useSearch, useSetSearch } from './use-search-params';
|
|
10
|
+
export { useBlocker } from './use-blocker';
|
|
10
11
|
export { default as defaultResolveView } from './resolve-view';
|
|
11
12
|
export * from './types';
|
|
12
13
|
export { hydrate } from './ssr';
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnchorHTMLAttributes, ComponentType, CSSProperties, DetailedHTMLProps, ReactNode } from 'react';
|
|
1
|
+
import type { AnchorHTMLAttributes, ComponentPropsWithRef, ComponentPropsWithoutRef, ComponentType, CSSProperties, DetailedHTMLProps, ElementType, ReactNode } from 'react';
|
|
2
2
|
import type { BaseRoute, ExtractPathParams, Matched, Location, RouterInstance, SearchInput } from '@native-router/core';
|
|
3
3
|
export type ResolveViewContext<R extends BaseRoute> = {
|
|
4
4
|
router: RouterInstance<R>;
|
|
@@ -193,6 +193,68 @@ export type LinkProps = {
|
|
|
193
193
|
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
194
194
|
children?: ReactNode;
|
|
195
195
|
} & DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
|
|
196
|
+
/**
|
|
197
|
+
* Keys owned by the link components themselves when rendering through an
|
|
198
|
+
* `as` component: the injection surface(`href`, the composed `onClick`,
|
|
199
|
+
* NavLink's `aria-current`) plus React's reserved keys and the polymorphism
|
|
200
|
+
* props' own names. They are stripped from the flattened `as`-props region
|
|
201
|
+
* and from `asProps`, so a same-named prop of the `as` component can never
|
|
202
|
+
* interfere with the navigation semantics.
|
|
203
|
+
*/
|
|
204
|
+
type AsManagedKeys = 'as' | 'asProps' | 'ref' | 'key' | 'href' | 'onClick' | 'aria-current';
|
|
205
|
+
/**
|
|
206
|
+
* Union-preserving `Omit`: the built-in collapses unions(`Omit<A | B, K>`
|
|
207
|
+
* picks across the members), which would flatten the discriminated union
|
|
208
|
+
* of {@link TypedLinkProps}.
|
|
209
|
+
*/
|
|
210
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
211
|
+
/**
|
|
212
|
+
* Union of every key appearing on any member: `keyof (A | B)` intersects
|
|
213
|
+
* the members' keys, so a key living on only part of a union(`params` on
|
|
214
|
+
* {@link TypedLinkProps}) would escape an `Omit` keyed on plain `keyof`.
|
|
215
|
+
*/
|
|
216
|
+
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
|
|
217
|
+
/**
|
|
218
|
+
* The `ref` an `as` component accepts — `{ref?: never}` when it does not
|
|
219
|
+
* take one(i.e. is not wrapped in `forwardRef`), so passing a ref to such
|
|
220
|
+
* a component is a compile error. The detection relies on how
|
|
221
|
+
* `ComponentPropsWithRef` is typed: guaranteed under `@types/react` ≥ 19;
|
|
222
|
+
* under `@types/react` 18 it always includes `ref`, so the guard degrades
|
|
223
|
+
* to accepting the ref(and React warns at runtime that it is dropped).
|
|
224
|
+
*/
|
|
225
|
+
type AsRefProps<A extends ElementType> = 'ref' extends keyof ComponentPropsWithRef<A> ? Pick<ComponentPropsWithRef<A>, 'ref'> : {
|
|
226
|
+
ref?: never;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Props of the link family({@link Link}, {@link NavLink},
|
|
230
|
+
* {@link PrefetchLink}, {@link TypedLink}) when rendering through a custom
|
|
231
|
+
* `as` component. Three regions, no ambiguity:
|
|
232
|
+
*
|
|
233
|
+
* 1. The component's own props(`Base`: `to`, anchor attributes, NavLink's
|
|
234
|
+
* active-state callbacks, ...) minus its anchor `ref`.
|
|
235
|
+
* 2. The flattened `as`-props region: every prop of the `as` component
|
|
236
|
+
* that does not collide with `Base`(`variant`, `tone`, ...) is accepted
|
|
237
|
+
* directly on the link. A key appearing on any member of a union
|
|
238
|
+
* `Base` counts as colliding(`params` on {@link TypedLinkProps} is
|
|
239
|
+
* owned by the link, never by the `as` component).
|
|
240
|
+
* 3. The `asProps` escape hatch for the colliding keys: only the props the
|
|
241
|
+
* `as` component shares with `Base`(`title`, `target`, ...) may be set
|
|
242
|
+
* there — `Pick` degrades to `{}` when there is no overlap — minus the
|
|
243
|
+
* managed keys(see {@link AsManagedKeys}), and it is spread last at
|
|
244
|
+
* runtime, explicitly overriding the base value.
|
|
245
|
+
*
|
|
246
|
+
* `href`, the composed `onClick` and NavLink's `aria-current` are always
|
|
247
|
+
* injected by the link itself(see {@link AsManagedKeys}) and win over
|
|
248
|
+
* everything else — neither the flattened region nor `asProps` can set
|
|
249
|
+
* them; the `ref` is the `as` component's own, so it must be
|
|
250
|
+
* ref-forwarding for `ref` to type-check.
|
|
251
|
+
* @group Types
|
|
252
|
+
* @category Link
|
|
253
|
+
*/
|
|
254
|
+
export type AsLinkProps<Base, A extends ElementType> = {
|
|
255
|
+
as?: A;
|
|
256
|
+
asProps?: Omit<Pick<ComponentPropsWithoutRef<A>, keyof Base & keyof ComponentPropsWithoutRef<A>>, AsManagedKeys>;
|
|
257
|
+
} & DistributiveOmit<Base, 'ref'> & Omit<ComponentPropsWithoutRef<A>, KeysOfUnion<Base> | AsManagedKeys> & AsRefProps<A>;
|
|
196
258
|
export type { SearchInput, SearchOutputOf, StandardSchemaV1 } from '@native-router/core';
|
|
197
259
|
/**
|
|
198
260
|
* Active state passed to the render-prop / callback flavors of
|
|
@@ -217,4 +279,4 @@ export type NavLinkProps = {
|
|
|
217
279
|
/** `aria-current` value rendered while active. @default 'page' */
|
|
218
280
|
ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time';
|
|
219
281
|
children?: ReactNode | ((state: NavLinkState) => ReactNode);
|
|
220
|
-
} & Omit<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, 'className' | 'style' | 'children'
|
|
282
|
+
} & Omit<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, 'className' | 'style' | 'children'>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlockerFn } from '@native-router/core';
|
|
2
|
+
/**
|
|
3
|
+
* Block navigations away from the current page while the component is
|
|
4
|
+
* mounted — the unsaved-changes guard.
|
|
5
|
+
*
|
|
6
|
+
* The predicate is the core `setBlocker` veto: `(to, from) => boolean`
|
|
7
|
+
* over path strings(including search and hash), asked synchronously at
|
|
8
|
+
* the head of every navigation and before a history POP lands. Return
|
|
9
|
+
* `false` to veto: a vetoed navigation never starts and a vetoed POP is
|
|
10
|
+
* rewound. `refresh` and guard redirects are never blocked; the effect
|
|
11
|
+
* releases the blocker on unmount, so the guard lives exactly as long
|
|
12
|
+
* as the guarding component.
|
|
13
|
+
*
|
|
14
|
+
* The predicate is stored in a ref and re-synced on every render, so a
|
|
15
|
+
* navigation is always asked the latest closure — a `confirmed` flag it
|
|
16
|
+
* captured works without re-registering anything. SSR-safe: nothing
|
|
17
|
+
* here touches `window`, and the registration itself is an effect that
|
|
18
|
+
* never runs on the server.
|
|
19
|
+
*
|
|
20
|
+
* @group Hooks
|
|
21
|
+
* @param fn blocker predicate; `to` is the target path, `from` the
|
|
22
|
+
* current path
|
|
23
|
+
* @see {@link setBlocker}
|
|
24
|
+
*/
|
|
25
|
+
export declare function useBlocker(fn: BlockerFn): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@native-router/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@native-router/core": "^1.
|
|
65
|
+
"@native-router/core": "^1.7.0",
|
|
66
66
|
"history": "^5.3.0",
|
|
67
67
|
"use-sync-external-store": "^1.6.0"
|
|
68
68
|
},
|
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
"@linaria/vite": "^5.0.4",
|
|
79
79
|
"@testing-library/dom": "^10.4.1",
|
|
80
80
|
"@testing-library/react": "^16.3.2",
|
|
81
|
-
"@types/node": "^26.
|
|
81
|
+
"@types/node": "^26.3.0",
|
|
82
82
|
"@types/react": "^19.2.18",
|
|
83
|
-
"@types/react-dom": "^19.2.
|
|
83
|
+
"@types/react-dom": "^19.2.5",
|
|
84
84
|
"@vitejs/plugin-react": "^6.1.0",
|
|
85
85
|
"@vitest/coverage-v8": "^4.1.11",
|
|
86
86
|
"@vitest/ui": "^4.1.11",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"commitizen": "^4.3.2",
|
|
91
91
|
"core-js": "^3.50.0",
|
|
92
92
|
"cross-env": "^10.1.0",
|
|
93
|
-
"eslint": "^10.9.
|
|
93
|
+
"eslint": "^10.9.1",
|
|
94
94
|
"eslint-config-prettier": "^10.1.8",
|
|
95
95
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
96
96
|
"eslint-plugin-compat": "^7.0.2",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"typedoc-plugin-mark-react-functional-components": "^0.2.2",
|
|
116
116
|
"typedoc-plugin-missing-exports": "^4.1.4",
|
|
117
117
|
"typescript": "^6.0.3",
|
|
118
|
-
"typescript-eslint": "^8.
|
|
118
|
+
"typescript-eslint": "^8.68.0",
|
|
119
119
|
"vite": "^8.2.2",
|
|
120
120
|
"vitest": "^4.1.11"
|
|
121
121
|
}
|