@octanejs/tanstack-router 0.1.2

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/package.json +47 -0
  4. package/src/Await.tsrx +21 -0
  5. package/src/Await.tsrx.d.ts +6 -0
  6. package/src/CatchBoundary.tsrx +91 -0
  7. package/src/CatchBoundary.tsrx.d.ts +8 -0
  8. package/src/ClientOnly.tsrx +21 -0
  9. package/src/ClientOnly.tsrx.d.ts +3 -0
  10. package/src/Link.tsrx +32 -0
  11. package/src/Link.tsrx.d.ts +4 -0
  12. package/src/Match.tsrx +204 -0
  13. package/src/Match.tsrx.d.ts +2 -0
  14. package/src/MatchRoute.tsrx +32 -0
  15. package/src/MatchRoute.tsrx.d.ts +3 -0
  16. package/src/Matches.tsrx +58 -0
  17. package/src/Matches.tsrx.d.ts +2 -0
  18. package/src/Navigate.tsrx +14 -0
  19. package/src/Navigate.tsrx.d.ts +2 -0
  20. package/src/Outlet.tsrx +49 -0
  21. package/src/Outlet.tsrx.d.ts +2 -0
  22. package/src/RouteNotFound.tsrx +23 -0
  23. package/src/RouterProvider.tsrx +37 -0
  24. package/src/RouterProvider.tsrx.d.ts +8 -0
  25. package/src/SafeFragment.tsrx +12 -0
  26. package/src/SafeFragment.tsrx.d.ts +2 -0
  27. package/src/ScrollRestoration.tsrx +16 -0
  28. package/src/ScrollRestoration.tsrx.d.ts +2 -0
  29. package/src/Transitioner.tsrx +126 -0
  30. package/src/context.ts +27 -0
  31. package/src/history.ts +10 -0
  32. package/src/hooks.ts +225 -0
  33. package/src/index.ts +91 -0
  34. package/src/internal.ts +31 -0
  35. package/src/lazyRouteComponent.ts +56 -0
  36. package/src/link.ts +383 -0
  37. package/src/not-found.tsrx +39 -0
  38. package/src/not-found.tsrx.d.ts +7 -0
  39. package/src/route.ts +147 -0
  40. package/src/router.ts +63 -0
  41. package/src/useAwaited.ts +9 -0
  42. package/src/useBlocker.tsrx +136 -0
  43. package/src/useBlocker.tsrx.d.ts +3 -0
  44. package/src/useElementScrollRestoration.ts +12 -0
  45. package/src/useRouterState.ts +17 -0
  46. package/src/useStore.ts +46 -0
  47. package/src/utils.ts +18 -0
@@ -0,0 +1,14 @@
1
+ // Imperative navigation as a component: navigates once on mount, renders nothing.
2
+ import { useLayoutEffect, useRef } from 'octane';
3
+ import { useNavigate } from './hooks.ts';
4
+
5
+ export function Navigate(props) @{
6
+ const navigate = useNavigate();
7
+ const done = useRef(false);
8
+
9
+ useLayoutEffect(() => {
10
+ if (done.current) return;
11
+ done.current = true;
12
+ navigate(props);
13
+ }, []);
14
+ }
@@ -0,0 +1,2 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ export declare const Navigate: (props: Record<string, unknown>) => unknown;
@@ -0,0 +1,49 @@
1
+ // Renders the child match below the current one. Reads the current match id from
2
+ // `matchContext`, finds the NEXT id in the match-id chain, and renders its `<Match/>`
3
+ // — the pull-based descent that replaces a top-down state diff. Renders nothing at
4
+ // a leaf.
5
+ //
6
+ // Not-found (react-router's Outlet, same order): when the URL matched no route,
7
+ // router-core flags ONE match `globalNotFound` — with the default
8
+ // `notFoundMode: 'fuzzy'` the deepest fuzzy-matched route that has children, with
9
+ // `notFoundMode: 'root'` the root. That match still renders its own component
10
+ // (the layout), and its `<Outlet/>` renders the not-found UI INSTEAD of a child
11
+ // match — so the 404 lands inside the layout chrome.
12
+ import { useContext, createElement, Suspense } from 'octane';
13
+ import { rootRouteId } from '@tanstack/router-core';
14
+ import { useStore } from './useStore.ts';
15
+ import { useRouter, matchContext } from './context.ts';
16
+ import { Match } from './Match.tsrx';
17
+ import { RouteNotFound } from './RouteNotFound.tsrx';
18
+ import { SafeFragment } from './SafeFragment.tsrx';
19
+
20
+ export function Outlet() @{
21
+ const router = useRouter();
22
+ const parentId = useContext(matchContext);
23
+ const parentStore = router.stores.matchStores.get(parentId);
24
+ const parentRouteId = useStore(parentStore, (m) => m?.routeId);
25
+ const globalNotFound = useStore(parentStore, (m) => m?.globalNotFound ?? false);
26
+ const childId = useStore(router.stores.matchesId, (ids) => {
27
+ const i = ids.indexOf(parentId);
28
+ return i >= 0 ? ids[i + 1] : undefined;
29
+ });
30
+
31
+ // The root route's outlet wraps the first real match in a Suspense boundary
32
+ // whose fallback is the router's defaultPendingComponent (react-router's
33
+ // Outlet does the same) — the outermost pending UI for the initial load.
34
+ const DefaultPending = router.options.defaultPendingComponent;
35
+ const RootSuspense =
36
+ parentRouteId === rootRouteId ? Suspense : SafeFragment;
37
+ const pendingElement =
38
+ DefaultPending ? createElement(DefaultPending, {}) : null;
39
+
40
+ @if (globalNotFound) {
41
+ <RouteNotFound routeId={parentRouteId} />
42
+ } @else {
43
+ @if (childId) {
44
+ <RootSuspense fallback={pendingElement}>
45
+ <Match matchId={childId} />
46
+ </RootSuspense>
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ export declare const Outlet: () => unknown;
@@ -0,0 +1,23 @@
1
+ // Renders a route's not-found UI — the port of react-router's
2
+ // `renderRouteNotFound` (+ its `DefaultGlobalNotFound`). Resolution order matches
3
+ // upstream: the route's own `notFoundComponent`, else the router-level
4
+ // `defaultNotFoundComponent`, else TanStack's generic `<p>Not Found</p>` (the
5
+ // upstream dev-only console.warn is not ported — repo policy: functional outcomes
6
+ // only). Rendered from two places, mirroring react-router: `<Outlet/>` when its
7
+ // match is flagged `globalNotFound` (the URL matched no route), and `<Match/>`
8
+ // when the match resolved with `status === 'notFound'` (a loader threw
9
+ // `notFound()`). Upstream spreads the NotFoundError onto the component, so a
10
+ // `notFound({ data })` payload arrives as the `data` prop.
11
+ import { useRouter } from './context.ts';
12
+
13
+ export function RouteNotFound(props) @{
14
+ const router = useRouter();
15
+ const route = router.routesById[props.routeId];
16
+ const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
17
+
18
+ @if (NotFound) {
19
+ <NotFound {...props.error ?? {}} />
20
+ } @else {
21
+ <p>Not Found</p>
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ // Places the router into context and renders the active match tree. Mirrors
2
+ // react-router's RouterProvider.tsx: `RouterContextProvider` is the low-level
3
+ // provider — it forwards any extra props into `router.update()` (so
4
+ // `<RouterProvider router={r} defaultPreload="intent">` reconfigures the
5
+ // instance), wraps in `router.options.Wrap` when configured, and provides the
6
+ // router; `RouterProvider` renders `<Matches/>` inside it.
7
+ import { hasKeys } from '@tanstack/router-core';
8
+ import { routerContext } from './context.ts';
9
+ import { Matches } from './Matches.tsrx';
10
+ import { SafeFragment } from './SafeFragment.tsrx';
11
+
12
+ export function RouterContextProvider(props) @{
13
+ const { router, children, ...rest } = props;
14
+ if (hasKeys(rest)) {
15
+ router.update({
16
+ ...router.options,
17
+ ...rest,
18
+ context: {
19
+ ...router.options.context,
20
+ ...rest.context,
21
+ },
22
+ });
23
+ }
24
+ const Wrap = router.options.Wrap ?? SafeFragment;
25
+
26
+ <Wrap>
27
+ <routerContext.Provider value={router}>{children}</routerContext.Provider>
28
+ </Wrap>
29
+ }
30
+
31
+ export function RouterProvider(props) @{
32
+ const { router, ...rest } = props;
33
+
34
+ <RouterContextProvider router={router} {...rest}>
35
+ <Matches />
36
+ </RouterContextProvider>
37
+ }
@@ -0,0 +1,8 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { AnyRouter } from '@tanstack/router-core';
3
+
4
+ export declare const RouterProvider: (props: { router: AnyRouter; children?: unknown }) => unknown;
5
+ export declare const RouterContextProvider: (props: {
6
+ router: import('@tanstack/router-core').AnyRouter;
7
+ children?: unknown;
8
+ }) => unknown;
@@ -0,0 +1,12 @@
1
+ // Pass-through wrapper used wherever react-router conditionally omits a boundary
2
+ // (Suspense / CatchBoundary / CatchNotFound / shellComponent). Rendering through
3
+ // SafeFragment instead of conditionally nesting the template keeps the boundary
4
+ // slot's component identity dynamic (`<Wrap>` where Wrap is Suspense or this) and
5
+ // — crucially — means a route WITHOUT a pendingComponent/errorComponent does not
6
+ // create a boundary at all, so suspensions and errors bubble to the nearest
7
+ // ancestor that has one (react-router parity).
8
+ export function SafeFragment(props) @{
9
+ <>
10
+ {props.children}
11
+ </>
12
+ }
@@ -0,0 +1,2 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ export declare const SafeFragment: (props: { children?: unknown }) => unknown;
@@ -0,0 +1,16 @@
1
+ // Restores scroll position across navigations. router-core's
2
+ // `setupScrollRestoration` does all the work (save on scroll keyed by location,
3
+ // restore on navigation) — the component just wires it on mount and renders nothing.
4
+ // You can also enable it via `createRouter({ scrollRestoration: true })` (handled in
5
+ // Matches); use one or the other.
6
+ import { useLayoutEffect } from 'octane';
7
+ import { setupScrollRestoration } from '@tanstack/router-core';
8
+ import { useRouter } from './context.ts';
9
+
10
+ export function ScrollRestoration() @{
11
+ const router = useRouter();
12
+ useLayoutEffect(() => {
13
+ setupScrollRestoration(router, true);
14
+ }, [router]);
15
+ <></>
16
+ }
@@ -0,0 +1,2 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ export declare const ScrollRestoration: () => unknown;
@@ -0,0 +1,126 @@
1
+ // The navigation engine (renders nothing) — port of react-router's Transitioner.
2
+ // It (1) supplies `router.startTransition` so every navigation state update rides
3
+ // an octane transition (concurrent navigation: the current page holds while the
4
+ // next route suspends), (2) subscribes to history so back/forward and `Link`
5
+ // clicks reload the matches, (3) kicks off the initial `router.load()` (skipped
6
+ // when hydrating SSR — ssr-client triggers it) and commits a canonical-URL
7
+ // replace when the mounted URL isn't in canonical form, and (4) drives the router
8
+ // EVENT LIFECYCLE: `onLoad` when a load settles, `onBeforeRouteMount` /
9
+ // `onResolved` when all pending work (load + transition + pending matches)
10
+ // drains, then commits `status: 'idle'` + `resolvedLocation`. router-core's
11
+ // scroll restoration, devtools, and `router.subscribe` consumers all key off
12
+ // these events, and `useRouterState` selectors on `resolvedLocation` depend on
13
+ // the commit.
14
+ import { useEffect, useLayoutEffect, useRef, useState, startTransition } from 'octane';
15
+ import { batch } from '@tanstack/store';
16
+ import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
17
+ import { useRouter } from './context.ts';
18
+ import { useStore } from './useStore.ts';
19
+ import { usePrevious } from './utils.ts';
20
+
21
+ export function Transitioner() @{
22
+ const router = useRouter();
23
+ const mountLoadForRouter = useRef({ router, mounted: false });
24
+ const [isTransitioning, setIsTransitioning] = useState(false);
25
+
26
+ const isLoading = useStore(router.stores.isLoading, (v) => v);
27
+ const hasPending = useStore(router.stores.hasPending, (v) => v);
28
+
29
+ const previousIsLoading = usePrevious(isLoading);
30
+ const isAnyPending = isLoading || isTransitioning || hasPending;
31
+ const previousIsAnyPending = usePrevious(isAnyPending);
32
+ const isPagePending = isLoading || hasPending;
33
+ const previousIsPagePending = usePrevious(isPagePending);
34
+
35
+ router.startTransition = (fn) => {
36
+ setIsTransitioning(true);
37
+ startTransition(() => {
38
+ fn();
39
+ setIsTransitioning(false);
40
+ });
41
+ };
42
+
43
+ // Subscribe to location changes and load the new location. On mount, also
44
+ // verify the current URL is in canonical form (search serialization,
45
+ // trailing slash) and replace it if not — mirroring the server-side redirect
46
+ // check in router.beforeLoad.
47
+ useEffect(() => {
48
+ const unsub = router.history.subscribe(() => router.load());
49
+
50
+ const nextLocation = router.buildLocation({
51
+ to: router.latestLocation.pathname,
52
+ search: true,
53
+ params: true,
54
+ hash: true,
55
+ state: true,
56
+ _includeValidateSearch: true,
57
+ });
58
+ if (
59
+ trimPathRight(router.latestLocation.publicHref) !== trimPathRight(nextLocation.publicHref)
60
+ ) {
61
+ router.commitLocation({ ...nextLocation, replace: true });
62
+ }
63
+
64
+ return () => unsub();
65
+ }, [router, router.history]);
66
+
67
+ // Initial load. Skipped when hydrating from SSR (ssr-client triggers it) and
68
+ // on re-runs for the same router instance.
69
+ useLayoutEffect(() => {
70
+ if (
71
+ typeof window !== 'undefined' && router.ssr ||
72
+ mountLoadForRouter.current.router === router && mountLoadForRouter.current.mounted
73
+ ) {
74
+ return;
75
+ }
76
+ mountLoadForRouter.current = { router, mounted: true };
77
+ router.load().catch((err: unknown) => {
78
+ console.error(err);
79
+ });
80
+ }, [router]);
81
+
82
+ // The router was loading and now it's not — the new matches are in state.
83
+ useLayoutEffect(() => {
84
+ if (previousIsLoading && !isLoading) {
85
+ router.emit({
86
+ type: 'onLoad',
87
+ ...getLocationChangeInfo(
88
+ router.stores.location.get(),
89
+ router.stores.resolvedLocation.get(),
90
+ ),
91
+ });
92
+ }
93
+ }, [previousIsLoading, router, isLoading]);
94
+
95
+ useLayoutEffect(() => {
96
+ if (previousIsPagePending && !isPagePending) {
97
+ router.emit({
98
+ type: 'onBeforeRouteMount',
99
+ ...getLocationChangeInfo(
100
+ router.stores.location.get(),
101
+ router.stores.resolvedLocation.get(),
102
+ ),
103
+ });
104
+ }
105
+ }, [isPagePending, previousIsPagePending, router]);
106
+
107
+ // Everything pending has drained — resolve the navigation: emit onResolved and
108
+ // commit status idle + resolvedLocation (what `onRendered` and scroll
109
+ // restoration key off).
110
+ useLayoutEffect(() => {
111
+ if (previousIsAnyPending && !isAnyPending) {
112
+ const changeInfo = getLocationChangeInfo(
113
+ router.stores.location.get(),
114
+ router.stores.resolvedLocation.get(),
115
+ );
116
+ router.emit({ type: 'onResolved', ...changeInfo });
117
+
118
+ batch(() => {
119
+ router.stores.status.set('idle');
120
+ router.stores.resolvedLocation.set(router.stores.location.get());
121
+ });
122
+ }
123
+ }, [isAnyPending, previousIsAnyPending, router]);
124
+
125
+ <></>
126
+ }
package/src/context.ts ADDED
@@ -0,0 +1,27 @@
1
+ // Router + match contexts. `routerContext` carries the Router instance (provided
2
+ // by RouterProvider, read by every hook). `matchContext` carries the current
3
+ // match id down the render tree so `Outlet` can find the NEXT match to render —
4
+ // the pull-based chaining that replaces a top-down state diff.
5
+ import { createContext, useContext } from 'octane';
6
+ import type { AnyRouter } from '@tanstack/router-core';
7
+
8
+ export const routerContext = createContext<AnyRouter | undefined>(undefined);
9
+ export const getRouterContext = (): typeof routerContext => routerContext;
10
+
11
+ // The id of the nearest rendered match (undefined above the first match).
12
+ export const matchContext = createContext<string | undefined>(undefined);
13
+
14
+ // Resolve the active router: an explicitly-passed one wins, else the context.
15
+ // `useContext` is keyed by context identity (not a per-call-site slot), so it's
16
+ // safe to call from this binding code without a slot.
17
+ export function useRouter(...args: unknown[]): AnyRouter {
18
+ const opts = (args.length && typeof args[0] !== 'symbol' ? args[0] : undefined) as
19
+ | { router?: AnyRouter; warn?: boolean }
20
+ | undefined;
21
+ const ctx = useContext(routerContext);
22
+ const router = opts?.router ?? ctx;
23
+ if (!router && opts?.warn !== false) {
24
+ throw new Error('useRouter must be used inside a <RouterProvider> component!');
25
+ }
26
+ return router as AnyRouter;
27
+ }
package/src/history.ts ADDED
@@ -0,0 +1,10 @@
1
+ // `@tanstack/history` is a separate framework-agnostic dependency (the browser/
2
+ // hash/memory history abstractions). react-router re-exports it from `./history`;
3
+ // we mirror that so `@octanejs/tanstack-router/history` and the bare entry both resolve it.
4
+ export {
5
+ createHistory,
6
+ createBrowserHistory,
7
+ createHashHistory,
8
+ createMemoryHistory,
9
+ } from '@tanstack/history';
10
+ export type * from '@tanstack/history';
package/src/hooks.ts ADDED
@@ -0,0 +1,225 @@
1
+ // The read hooks — ports of react-router's useMatch.tsx / useParams / useSearch /
2
+ // useLoaderData / useLoaderDeps / useRouteContext / useNavigate / useCanGoBack /
3
+ // Matches.tsx (useMatches / useParentMatches / useChildMatches). Everything match-
4
+ // shaped funnels through `useMatch`, which subscribes to ONE match store:
5
+ // - `from` given → `router.stores.getRouteMatchStore(from)` (a cached computed
6
+ // that resolves a routeId to its current match);
7
+ // - no `from` → the NEAREST match via `matchContext` (the match id the enclosing
8
+ // `<Match>` provided) — NOT the leaf match.
9
+ // A missing match throws unless `shouldThrow: false` (upstream invariant).
10
+ // Selectors run through `useStructuralSharing` (replaceEqualDeep against the
11
+ // previous selection when `structuralSharing ?? defaultStructuralSharing`).
12
+ import { useContext, useRef, useCallback } from 'octane';
13
+ import { replaceEqualDeep } from '@tanstack/router-core';
14
+ import { useRouter, matchContext } from './context';
15
+ import { useStore } from './useStore';
16
+ import { splitSlot, subSlot } from './internal';
17
+
18
+ // Sentinel store + selection for "no match at this id" (upstream's dummyStore).
19
+ const dummyStore = {
20
+ get() {},
21
+ subscribe() {
22
+ return { unsubscribe() {} };
23
+ },
24
+ };
25
+
26
+ // Selector wrapper honoring structural sharing: when enabled, the selection is
27
+ // replaceEqualDeep'd against the previous one so deep-equal slices keep their
28
+ // reference (no re-render). Port of react-router's useStructuralSharing.
29
+ function useStructuralSharing(opts: any, router: any, slot: symbol | undefined) {
30
+ const previousResult = useRef<any>(undefined, subSlot(slot, 'ss'));
31
+ return (slice: any) => {
32
+ const selected = opts?.select ? opts.select(slice) : slice;
33
+ if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
34
+ return (previousResult.current = replaceEqualDeep(previousResult.current, selected));
35
+ }
36
+ return selected;
37
+ };
38
+ }
39
+
40
+ export function useMatch(...args: any[]): any {
41
+ const [user, slot] = splitSlot(args);
42
+ const opts = user[0] ?? {};
43
+ const router = useRouter();
44
+ // octane has no rules of hooks, so the nearest-match context is read
45
+ // unconditionally (upstream reads a dummy context when `from` is given).
46
+ const nearestMatchId = useContext(matchContext);
47
+ const matchStore = opts.from
48
+ ? router.stores.getRouteMatchStore(opts.from)
49
+ : router.stores.matchStores.get(nearestMatchId as string);
50
+
51
+ const selector = useStructuralSharing(opts, router, subSlot(slot, 'm'));
52
+ const matchSelection = useStore(
53
+ matchStore ?? dummyStore,
54
+ (match: any) => (match ? selector(match) : dummyStore),
55
+ undefined,
56
+ subSlot(slot, 'm:us'),
57
+ );
58
+
59
+ if (matchSelection !== dummyStore) return matchSelection;
60
+ if (opts.shouldThrow ?? true) {
61
+ throw new Error(
62
+ `Invariant failed: Could not find ${
63
+ opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'
64
+ }`,
65
+ );
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ export function useParams(...args: any[]): any {
71
+ const [user, slot] = splitSlot(args);
72
+ const opts = user[0] ?? {};
73
+ return useMatch(
74
+ {
75
+ from: opts.from,
76
+ strict: opts.strict,
77
+ shouldThrow: opts.shouldThrow,
78
+ structuralSharing: opts.structuralSharing,
79
+ select: (match: any) => {
80
+ const params = opts.strict === false ? match.params : match._strictParams;
81
+ return opts.select ? opts.select(params) : params;
82
+ },
83
+ },
84
+ subSlot(slot, 'params'),
85
+ );
86
+ }
87
+
88
+ export function useSearch(...args: any[]): any {
89
+ const [user, slot] = splitSlot(args);
90
+ const opts = user[0] ?? {};
91
+ return useMatch(
92
+ {
93
+ from: opts.from,
94
+ strict: opts.strict,
95
+ shouldThrow: opts.shouldThrow,
96
+ structuralSharing: opts.structuralSharing,
97
+ select: (match: any) => (opts.select ? opts.select(match.search) : match.search),
98
+ },
99
+ subSlot(slot, 'search'),
100
+ );
101
+ }
102
+
103
+ export function useLoaderData(...args: any[]): any {
104
+ const [user, slot] = splitSlot(args);
105
+ const opts = user[0] ?? {};
106
+ return useMatch(
107
+ {
108
+ from: opts.from,
109
+ strict: opts.strict,
110
+ structuralSharing: opts.structuralSharing,
111
+ select: (match: any) => (opts.select ? opts.select(match.loaderData) : match.loaderData),
112
+ },
113
+ subSlot(slot, 'loader'),
114
+ );
115
+ }
116
+
117
+ export function useLoaderDeps(...args: any[]): any {
118
+ const [user, slot] = splitSlot(args);
119
+ const opts = user[0] ?? {};
120
+ const { select, ...rest } = opts;
121
+ return useMatch(
122
+ {
123
+ ...rest,
124
+ select: (match: any) => (select ? select(match.loaderDeps) : match.loaderDeps),
125
+ },
126
+ subSlot(slot, 'deps'),
127
+ );
128
+ }
129
+
130
+ export function useRouteContext(...args: any[]): any {
131
+ const [user, slot] = splitSlot(args);
132
+ const opts = user[0] ?? {};
133
+ return useMatch(
134
+ {
135
+ ...opts,
136
+ select: (match: any) => (opts.select ? opts.select(match.context) : match.context),
137
+ },
138
+ subSlot(slot, 'ctx'),
139
+ );
140
+ }
141
+
142
+ export function useLocation(...args: any[]): any {
143
+ const [user, slot] = splitSlot(args);
144
+ const opts = user[0] ?? {};
145
+ const router = useRouter();
146
+ return useStore(
147
+ router.stores.location,
148
+ useStructuralSharing(opts, router, subSlot(slot, 'loc')),
149
+ undefined,
150
+ subSlot(slot, 'loc:us'),
151
+ );
152
+ }
153
+
154
+ export function useMatches(...args: any[]): any {
155
+ const [user, slot] = splitSlot(args);
156
+ const opts = user[0] ?? {};
157
+ const router = useRouter();
158
+ return useStore(
159
+ router.stores.matches,
160
+ useStructuralSharing(opts, router, subSlot(slot, 'matches')),
161
+ undefined,
162
+ subSlot(slot, 'matches:us'),
163
+ );
164
+ }
165
+
166
+ export function useParentMatches(...args: any[]): any {
167
+ const [user, slot] = splitSlot(args);
168
+ const opts = user[0] ?? {};
169
+ const contextMatchId = useContext(matchContext);
170
+ return useMatches(
171
+ {
172
+ select: (matches: any[]) => {
173
+ matches = matches.slice(
174
+ 0,
175
+ matches.findIndex((d: any) => d.id === contextMatchId),
176
+ );
177
+ return opts.select ? opts.select(matches) : matches;
178
+ },
179
+ structuralSharing: opts.structuralSharing,
180
+ },
181
+ subSlot(slot, 'parents'),
182
+ );
183
+ }
184
+
185
+ export function useChildMatches(...args: any[]): any {
186
+ const [user, slot] = splitSlot(args);
187
+ const opts = user[0] ?? {};
188
+ const contextMatchId = useContext(matchContext);
189
+ return useMatches(
190
+ {
191
+ select: (matches: any[]) => {
192
+ matches = matches.slice(matches.findIndex((d: any) => d.id === contextMatchId) + 1);
193
+ return opts.select ? opts.select(matches) : matches;
194
+ },
195
+ structuralSharing: opts.structuralSharing,
196
+ },
197
+ subSlot(slot, 'children'),
198
+ );
199
+ }
200
+
201
+ // Returns a STABLE navigate function (upstream useCallback([from, router])) that
202
+ // forwards to `router.navigate`, defaulting `from` to the hook's option.
203
+ export function useNavigate(...args: any[]): (to: any) => any {
204
+ const [user, slot] = splitSlot(args);
205
+ const opts = user[0] ?? {};
206
+ const router = useRouter(opts.router ? { router: opts.router } : undefined);
207
+ return useCallback(
208
+ (options: any) => router.navigate({ ...options, from: options?.from ?? opts.from }),
209
+ [opts.from, router],
210
+ subSlot(slot, 'nav'),
211
+ );
212
+ }
213
+
214
+ // True when the current history entry isn't the first (there is somewhere to go
215
+ // back to) — per upstream useCanGoBack (location.state.__TSR_index !== 0).
216
+ export function useCanGoBack(...args: any[]): boolean {
217
+ const [, slot] = splitSlot(args);
218
+ const router = useRouter();
219
+ return useStore(
220
+ router.stores.location,
221
+ (location: any) => location.state.__TSR_index !== 0,
222
+ undefined,
223
+ subSlot(slot, 'back'),
224
+ );
225
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ // @octanejs/tanstack-router — TanStack Router for the octane renderer.
2
+ //
3
+ // TanStack Router splits a framework-agnostic core (`@tanstack/router-core`: the
4
+ // Router/route-tree/matching/history/reactive-store) from a thin React binding
5
+ // (`@tanstack/react-router`). Mirroring @octanejs/tanstack-query, this package re-exports
6
+ // the core VERBATIM and reimplements only the React binding on octane's hooks. The
7
+ // load-bearing seam is router-core's reactive store: `createRouter` supplies the
8
+ // CLIENT store factory (`createAtom`/`batch` from `@tanstack/store`), whose atoms
9
+ // expose `.subscribe`/`.get` — bound to octane's `useSyncExternalStore` by
10
+ // `useStore`. The match tree renders pull-based: `RouterProvider` → first match →
11
+ // each route's `<Outlet/>` looks up the NEXT match via `matchContext`.
12
+ //
13
+ // Scope: code-based routing at react-router parity — RouterProvider (+
14
+ // RouterContextProvider/Wrap/InnerWrap), the full Match pipeline (Suspense /
15
+ // CatchBoundary / CatchNotFound per route, pending/error/redirect statuses,
16
+ // remountDeps, shellComponent), the router event lifecycle
17
+ // (onLoad/onBeforeRouteMount/onResolved/onRendered + resolvedLocation — scroll
18
+ // restoration restores off it), Link with preloading/masking/active-options,
19
+ // createLink/useLinkProps, navigation blocking (useBlocker/Block), the full
20
+ // read-hook set (useMatch and friends, nearest-match resolution via
21
+ // matchContext), Route/getRouteApi hook accessors, Await/useAwaited, lazy
22
+ // routes, and search validation/middleware from core. Deferred: file-based
23
+ // routing + codegen (`createFileRoute`, @tanstack/router-plugin), SSR entries
24
+ // (RouterServer/RouterClient, HeadContent/Scripts), and devtools.
25
+ export * from '@tanstack/router-core';
26
+ export {
27
+ createHistory,
28
+ createBrowserHistory,
29
+ createHashHistory,
30
+ createMemoryHistory,
31
+ } from './history';
32
+ // History types on the main entry (upstream parity). `NavigateOptions` is NOT
33
+ // re-exported from history — router-core's richer NavigateOptions wins.
34
+ export type {
35
+ RouterHistory,
36
+ HistoryLocation,
37
+ ParsedPath,
38
+ HistoryState,
39
+ ParsedHistoryState,
40
+ HistoryAction,
41
+ BlockerFnArgs,
42
+ BlockerFn,
43
+ NavigationBlocker,
44
+ } from '@tanstack/history';
45
+
46
+ export { createRouter, Router } from './router';
47
+ export {
48
+ createRoute,
49
+ createRootRoute,
50
+ createRootRouteWithContext,
51
+ createRouteMask,
52
+ getRouteApi,
53
+ Route,
54
+ RootRoute,
55
+ RouteApi,
56
+ } from './route';
57
+ export { routerContext, getRouterContext, matchContext, useRouter } from './context';
58
+ export { useStore } from './useStore';
59
+ export { useRouterState } from './useRouterState';
60
+ export {
61
+ useMatch,
62
+ useLocation,
63
+ useParams,
64
+ useSearch,
65
+ useLoaderData,
66
+ useLoaderDeps,
67
+ useRouteContext,
68
+ useMatches,
69
+ useParentMatches,
70
+ useChildMatches,
71
+ useNavigate,
72
+ useCanGoBack,
73
+ } from './hooks';
74
+ export { useAwaited } from './useAwaited';
75
+ export { useLinkProps, createLink, linkOptions } from './link';
76
+ export { useBlocker, Block } from './useBlocker.tsrx';
77
+ export { useMatchRoute, MatchRoute } from './MatchRoute.tsrx';
78
+ export { useElementScrollRestoration } from './useElementScrollRestoration';
79
+ export { lazyRouteComponent } from './lazyRouteComponent';
80
+
81
+ export { RouterProvider, RouterContextProvider } from './RouterProvider.tsrx';
82
+ export { Outlet } from './Outlet.tsrx';
83
+ export { Link } from './Link.tsrx';
84
+ export { Navigate } from './Navigate.tsrx';
85
+ export { Await } from './Await.tsrx';
86
+ export { ScrollRestoration } from './ScrollRestoration.tsrx';
87
+ export { Matches } from './Matches.tsrx';
88
+ export { Match } from './Match.tsrx';
89
+ export { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
90
+ export { CatchNotFound, DefaultGlobalNotFound } from './not-found.tsrx';
91
+ export { ClientOnly, useHydrated } from './ClientOnly.tsrx';