@octanejs/tanstack-router 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/tanstack-router",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -43,16 +43,16 @@
43
43
  "@tanstack/store": "^0.9.3"
44
44
  },
45
45
  "peerDependencies": {
46
- "octane": "0.1.10"
46
+ "octane": "0.1.11"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@tanstack/react-router": "1.170.16",
50
- "@tsrx/react": "^0.2.37",
50
+ "@tsrx/react": "^0.2.44",
51
51
  "esbuild": "^0.28.1",
52
52
  "react": "^19.2.0",
53
53
  "react-dom": "^19.2.0",
54
54
  "vitest": "^4.1.9",
55
- "octane": "0.1.10"
55
+ "octane": "0.1.11"
56
56
  },
57
57
  "scripts": {
58
58
  "test": "vitest run"
package/src/Await.tsrx CHANGED
@@ -4,13 +4,18 @@
4
4
  // `render` prop (re-passing it as `{children}` would be an identifier, not a literal
5
5
  // render-prop, so the consumer's `<Await>{(d) => …}</Await>` stays the public API).
6
6
  import { use } from 'octane';
7
+ import type { OctaneNode } from 'octane';
7
8
 
8
- function AwaitInner(props) {
9
+ function AwaitInner(props: { promise: Promise<any>; render: (data: any) => OctaneNode }) {
9
10
  const data = use(props.promise);
10
11
  return props.render(data);
11
12
  }
12
13
 
13
- export function Await(props) @{
14
+ export function Await(props: {
15
+ promise: Promise<any>;
16
+ fallback?: OctaneNode;
17
+ children: (data: any) => OctaneNode;
18
+ }) @{
14
19
  @try {
15
20
  <AwaitInner promise={props.promise} render={props.children} />
16
21
  } @pending {
@@ -14,8 +14,15 @@
14
14
  // commit later, same observable outcome). Match passes the router's `loadedAt`
15
15
  // so navigation clears route error UI.
16
16
  import { useState, useRef, useLayoutEffect } from 'octane';
17
+ import type { OctaneNode } from 'octane';
18
+ import type { ErrorInfo, ErrorRouteComponent } from './route.ts';
17
19
 
18
- export function CatchBoundary(props) @{
20
+ export function CatchBoundary(props: {
21
+ getResetKey: () => number | string;
22
+ children: OctaneNode;
23
+ errorComponent?: ErrorRouteComponent;
24
+ onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
25
+ }) @{
19
26
  @try {
20
27
  <>
21
28
  {props.children}
@@ -31,10 +38,16 @@ export function CatchBoundary(props) @{
31
38
  }
32
39
  }
33
40
 
34
- function CatchErrorView(props) @{
41
+ function CatchErrorView(props: {
42
+ error: any;
43
+ reset: () => void;
44
+ getResetKey?: () => number | string;
45
+ errorComponent?: ErrorRouteComponent;
46
+ onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
47
+ }) @{
35
48
  // componentDidCatch parity — once per distinct caught error, during render so a
36
49
  // rethrow (the notFound-forwarding path) escapes to the parent boundary.
37
- const reported = useRef(null);
50
+ const reported = useRef<unknown>(null);
38
51
  if (reported.current !== props.error) {
39
52
  reported.current = props.error;
40
53
  if (props.onCatch) props.onCatch(props.error, { componentStack: '' });
@@ -60,7 +73,11 @@ function CatchErrorView(props) @{
60
73
  // The default error UI, markup-identical to react-router's ErrorComponent (string
61
74
  // styles serialize to the same inline css React produces, so the differential rig
62
75
  // can byte-compare it).
63
- export function ErrorComponent(props) @{
76
+ export function ErrorComponent(props: {
77
+ error: any;
78
+ reset?: () => void;
79
+ info?: { componentStack?: string };
80
+ }) @{
64
81
  const [show, setShow] = useState(process.env.NODE_ENV !== 'production');
65
82
 
66
83
  <div style="padding:.5rem;max-width:100%">
@@ -1,8 +1,14 @@
1
1
  // Type declaration for the .tsrx components (resolved by relative path).
2
+ import type { ErrorInfo, ErrorRouteComponent } from './route';
3
+
2
4
  export declare const CatchBoundary: (props: {
3
- getResetKey?: () => number | string;
4
- errorComponent?: unknown;
5
- onCatch?: (error: any, errorInfo: { componentStack: string }) => void;
5
+ getResetKey: () => number | string;
6
+ errorComponent?: ErrorRouteComponent;
7
+ onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
6
8
  children?: unknown;
7
9
  }) => unknown;
8
- export declare const ErrorComponent: (props: { error: any; reset?: () => void }) => unknown;
10
+ export declare const ErrorComponent: (props: {
11
+ error: any;
12
+ reset?: () => void;
13
+ info?: { componentStack?: string };
14
+ }) => unknown;
@@ -6,6 +6,7 @@
6
6
  // compiler slots the hook calls (and withSlot-disambiguates useHydrated's
7
7
  // call sites).
8
8
  import { useSyncExternalStore } from 'octane';
9
+ import type { OctaneNode } from 'octane';
9
10
 
10
11
  const subscribe = () => () => {};
11
12
 
@@ -13,7 +14,7 @@ export function useHydrated() {
13
14
  return useSyncExternalStore(subscribe, () => true, () => false);
14
15
  }
15
16
 
16
- export function ClientOnly(props) @{
17
+ export function ClientOnly(props: { children?: OctaneNode; fallback?: OctaneNode }) @{
17
18
  const hydrated = useHydrated();
18
19
  <>
19
20
  {hydrated ? props.children : props.fallback ?? null}
package/src/Link.tsrx CHANGED
@@ -11,7 +11,10 @@
11
11
  import { isChildrenBlock } from 'octane';
12
12
  import { useLinkProps } from './link.ts';
13
13
 
14
- export function Link(props) @{
14
+ // Props are permissive (`any`) at the implementation boundary — upstream's Link
15
+ // implementation is likewise `(props: any)` behind the generic `LinkComponent<'a'>`
16
+ // facade; the full type-safe `to`/`params`/`search` surface is a follow-up.
17
+ export function Link(props: any) @{
15
18
  const { _asChild: AsChild, children, ...rest } = props;
16
19
  const linkProps = useLinkProps(rest);
17
20
  const { disabled: _disabled, ...aProps } = linkProps;
package/src/Match.tsrx CHANGED
@@ -24,6 +24,8 @@ import {
24
24
  isNotFound,
25
25
  rootRouteId,
26
26
  } from '@tanstack/router-core';
27
+ import type { NotFoundError, ParsedLocation, RootRouteOptions } from '@tanstack/router-core';
28
+ import type { ErrorInfo, ErrorRouteComponent } from './route.ts';
27
29
  import { useStore } from './useStore.ts';
28
30
  import { useRouter, matchContext } from './context.ts';
29
31
  import { Outlet } from './Outlet.tsrx';
@@ -32,11 +34,11 @@ import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
32
34
  import { CatchNotFound } from './not-found.tsrx';
33
35
  import { SafeFragment } from './SafeFragment.tsrx';
34
36
 
35
- export function Match(props) @{
37
+ export function Match(props: { matchId: string }) @{
36
38
  const router = useRouter();
37
- const matchStore = router.stores.matchStores.get(props.matchId);
39
+ const matchStore = router.stores.matchStores.get(props.matchId)!;
38
40
  const resetKey = useStore(router.stores.loadedAt, (l) => l);
39
- const routeId = useStore(matchStore, (m) => m.routeId);
41
+ const routeId = useStore(matchStore, (m) => m.routeId as string);
40
42
  const route = router.routesById[routeId];
41
43
 
42
44
  const PendingComponent = route.options.pendingComponent ?? router.options.defaultPendingComponent;
@@ -50,12 +52,15 @@ export function Match(props) @{
50
52
  // Suspense boundary when explicitly opted in via wrapInSuspense.
51
53
  const SuspenseWrap =
52
54
  (!route.isRoot || route.options.wrapInSuspense) &&
53
- (route.options.wrapInSuspense ?? PendingComponent ?? routeErrorComponent?.preload)
55
+ (route.options.wrapInSuspense ?? PendingComponent ??
56
+ (routeErrorComponent as ErrorRouteComponent | undefined)?.preload)
54
57
  ? Suspense
55
58
  : SafeFragment;
56
59
  const CatchWrap = routeErrorComponent ? CatchBoundary : SafeFragment;
57
60
  const NotFoundWrap = routeNotFoundComponent ? CatchNotFound : SafeFragment;
58
- const ShellComponent = route.isRoot ? route.options.shellComponent ?? SafeFragment : SafeFragment;
61
+ const ShellComponent = route.isRoot
62
+ ? (route.options as RootRouteOptions).shellComponent ?? SafeFragment
63
+ : SafeFragment;
59
64
 
60
65
  const pendingElement =
61
66
  PendingComponent ? createElement(PendingComponent, {}) : null;
@@ -67,7 +72,7 @@ export function Match(props) @{
67
72
  <CatchWrap
68
73
  getResetKey={() => resetKey}
69
74
  errorComponent={routeErrorComponent || ErrorComponent}
70
- onCatch={(error, errorInfo) => {
75
+ onCatch={(error: Error, errorInfo: ErrorInfo) => {
71
76
  if (isNotFound(error)) {
72
77
  error.routeId ??= routeId;
73
78
  throw error;
@@ -76,7 +81,7 @@ export function Match(props) @{
76
81
  }}
77
82
  >
78
83
  <NotFoundWrap
79
- fallback={(error) => {
84
+ fallback={(error: NotFoundError) => {
80
85
  error.routeId ??= routeId;
81
86
 
82
87
  if (
@@ -85,7 +90,7 @@ export function Match(props) @{
85
90
  ) {
86
91
  throw error;
87
92
  }
88
- return createElement(routeNotFoundComponent, error);
93
+ return createElement(routeNotFoundComponent, error as any);
89
94
  }}
90
95
  >
91
96
  <MatchInner matchId={props.matchId} />
@@ -99,24 +104,24 @@ export function Match(props) @{
99
104
  </ShellComponent>
100
105
  }
101
106
 
102
- function MatchInner(props) {
107
+ function MatchInner(props: { matchId: string }) {
103
108
  const router = useRouter();
104
- const matchStore = router.stores.matchStores.get(props.matchId);
109
+ const matchStore = router.stores.matchStores.get(props.matchId)!;
105
110
  const match = useStore(matchStore, (m) => m);
106
- const routeId = match.routeId;
111
+ const routeId = match.routeId as string;
107
112
  const route = router.routesById[routeId];
108
113
 
109
114
  // The live match in the router (if still mounted there) wins over the
110
115
  // snapshot for promise lookups, per upstream getMatchPromise.
111
- const getMatchPromise = (key) => router.getMatch(match.id)?._nonReactive[key] ??
112
- match._nonReactive[key];
116
+ const getMatchPromise = (key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise') =>
117
+ router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key];
113
118
 
114
119
  if (match._displayPending) {
115
- use(getMatchPromise('displayPendingPromise'));
120
+ use(getMatchPromise('displayPendingPromise')!);
116
121
  }
117
122
 
118
123
  if (match._forcePending) {
119
- use(getMatchPromise('minPendingPromise'));
124
+ use(getMatchPromise('minPendingPromise')!);
120
125
  }
121
126
 
122
127
  if (match.status === 'pending') {
@@ -125,7 +130,7 @@ function MatchInner(props) {
125
130
  if (pendingMinMs) {
126
131
  const routerMatch = router.getMatch(match.id);
127
132
  if (routerMatch && !routerMatch._nonReactive.minPendingPromise) {
128
- const minPendingPromise = createControlledPromise();
133
+ const minPendingPromise = createControlledPromise<void>();
129
134
  routerMatch._nonReactive.minPendingPromise = minPendingPromise;
130
135
  setTimeout(() => {
131
136
  minPendingPromise.resolve();
@@ -133,7 +138,7 @@ function MatchInner(props) {
133
138
  }, pendingMinMs);
134
139
  }
135
140
  }
136
- use(getMatchPromise('loadPromise'));
141
+ use(getMatchPromise('loadPromise')!);
137
142
  }
138
143
 
139
144
  if (match.status === 'notFound') {
@@ -143,7 +148,7 @@ function MatchInner(props) {
143
148
  if (match.status === 'redirected') {
144
149
  // Observed mid-transition while a redirect is in flight — suspend on the
145
150
  // load so this stale render is abandoned and the redirect completes.
146
- use(getMatchPromise('loadPromise'));
151
+ use(getMatchPromise('loadPromise')!);
147
152
  }
148
153
 
149
154
  if (match.status === 'error') {
@@ -174,7 +179,7 @@ function MatchInner(props) {
174
179
  // `resolvedLocation` to the new location.
175
180
  function OnRendered() @{
176
181
  const router = useRouter();
177
- const prevResolvedLocationRef = useRef(undefined);
182
+ const prevResolvedLocationRef = useRef<ParsedLocation<any> | undefined>(undefined);
178
183
  const renderedLocationKey = useStore(
179
184
  router.stores.resolvedLocation,
180
185
  (loc) => loc?.state.__TSR_key,
@@ -11,13 +11,16 @@ import { useStore } from './useStore.ts';
11
11
  export function useMatchRoute() {
12
12
  const router = useRouter();
13
13
  useStore(router.stores.matchRouteDeps, (d) => d);
14
- return useCallback((opts) => {
14
+ return useCallback((opts: Record<string, any>) => {
15
15
  const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts;
16
- return router.matchRoute(rest, { pending, caseSensitive, fuzzy, includeSearch });
16
+ return router.matchRoute(rest as any, { pending, caseSensitive, fuzzy, includeSearch });
17
17
  }, [router]);
18
18
  }
19
19
 
20
- export function MatchRoute(props) @{
20
+ // Props are permissive (`Record<string, any>` + optional render-prop children)
21
+ // upstream's MakeMatchRouteOptions generics are the type-safe facade; the octane
22
+ // binding keeps the loose v1 surface (see Link).
23
+ export function MatchRoute(props: Record<string, any>) @{
21
24
  const matchRoute = useMatchRoute();
22
25
  const params = matchRoute(props);
23
26
  const out =
package/src/Navigate.tsrx CHANGED
@@ -1,8 +1,9 @@
1
1
  // Imperative navigation as a component: navigates once on mount, renders nothing.
2
2
  import { useLayoutEffect, useRef } from 'octane';
3
+ import type { NavigateOptions } from '@tanstack/router-core';
3
4
  import { useNavigate } from './hooks.ts';
4
5
 
5
- export function Navigate(props) @{
6
+ export function Navigate(props: NavigateOptions) @{
6
7
  const navigate = useNavigate();
7
8
  const done = useRef(false);
8
9
 
package/src/Outlet.tsrx CHANGED
@@ -19,9 +19,11 @@ import { SafeFragment } from './SafeFragment.tsrx';
19
19
 
20
20
  export function Outlet() @{
21
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);
22
+ // Outlet only renders inside a match's component, so the context id (and its
23
+ // pooled store) are present — same invariant upstream asserts with invariant().
24
+ const parentId = useContext(matchContext)!;
25
+ const parentStore = router.stores.matchStores.get(parentId)!;
26
+ const parentRouteId = useStore(parentStore, (m) => m?.routeId as string | undefined);
25
27
  const globalNotFound = useStore(parentStore, (m) => m?.globalNotFound ?? false);
26
28
  const childId = useStore(router.stores.matchesId, (ids) => {
27
29
  const i = ids.indexOf(parentId);
@@ -38,7 +40,7 @@ export function Outlet() @{
38
40
  DefaultPending ? createElement(DefaultPending, {}) : null;
39
41
 
40
42
  @if (globalNotFound) {
41
- <RouteNotFound routeId={parentRouteId} />
43
+ <RouteNotFound routeId={parentRouteId!} />
42
44
  } @else {
43
45
  @if (childId) {
44
46
  <RootSuspense fallback={pendingElement}>
@@ -10,7 +10,7 @@
10
10
  // `notFound({ data })` payload arrives as the `data` prop.
11
11
  import { useRouter } from './context.ts';
12
12
 
13
- export function RouteNotFound(props) @{
13
+ export function RouteNotFound(props: { routeId: string; error?: any }) @{
14
14
  const router = useRouter();
15
15
  const route = router.routesById[props.routeId];
16
16
  const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
@@ -5,11 +5,21 @@
5
5
  // instance), wraps in `router.options.Wrap` when configured, and provides the
6
6
  // router; `RouterProvider` renders `<Matches/>` inside it.
7
7
  import { hasKeys } from '@tanstack/router-core';
8
+ import type { AnyRouter } from '@tanstack/router-core';
9
+ import type { OctaneNode } from 'octane';
8
10
  import { routerContext } from './context.ts';
9
11
  import { Matches } from './Matches.tsrx';
10
12
  import { SafeFragment } from './SafeFragment.tsrx';
11
13
 
12
- export function RouterContextProvider(props) @{
14
+ // Upstream's `RouterProps` is `Omit<RouterOptions<…>, 'context'> & { router }` —
15
+ // extra props reconfigure the instance via `router.update()`. The octane binding
16
+ // keeps the router-typed member strict and the option passthrough permissive.
17
+ export type RouterProps = {
18
+ router: AnyRouter;
19
+ context?: Record<string, any>;
20
+ } & Record<string, any>;
21
+
22
+ export function RouterContextProvider(props: RouterProps & { children?: OctaneNode }) @{
13
23
  const { router, children, ...rest } = props;
14
24
  if (hasKeys(rest)) {
15
25
  router.update({
@@ -28,7 +38,7 @@ export function RouterContextProvider(props) @{
28
38
  </Wrap>
29
39
  }
30
40
 
31
- export function RouterProvider(props) @{
41
+ export function RouterProvider(props: RouterProps) @{
32
42
  const { router, ...rest } = props;
33
43
 
34
44
  <RouterContextProvider router={router} {...rest}>
@@ -1,8 +1,12 @@
1
1
  // Type declaration for the .tsrx component (resolved by relative path).
2
2
  import type { AnyRouter } from '@tanstack/router-core';
3
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;
4
+ export type RouterProps = {
5
+ router: AnyRouter;
6
+ context?: Record<string, any>;
7
+ } & Record<string, any>;
8
+
9
+ export declare const RouterProvider: (props: RouterProps) => unknown;
10
+ export declare const RouterContextProvider: (
11
+ props: RouterProps & { children?: unknown },
12
+ ) => unknown;
@@ -5,7 +5,10 @@
5
5
  // — crucially — means a route WITHOUT a pendingComponent/errorComponent does not
6
6
  // create a boundary at all, so suspensions and errors bubble to the nearest
7
7
  // ancestor that has one (react-router parity).
8
- export function SafeFragment(props) @{
8
+ // Props are `any` (upstream parity: react-router's `SafeFragment(props: any)`)
9
+ // it stands in for Suspense/CatchBoundary/CatchNotFound/shellComponent at dynamic
10
+ // boundary slots, so it must accept any boundary's props and render only children.
11
+ export function SafeFragment(props: any) @{
9
12
  <>
10
13
  {props.children}
11
14
  </>
package/src/index.ts CHANGED
@@ -54,6 +54,16 @@ export {
54
54
  RootRoute,
55
55
  RouteApi,
56
56
  } from './route';
57
+ // Framework-facing component types (react-router parity, on octane renderables).
58
+ // route.ts also narrows router-core's *Extensions interfaces to these via module
59
+ // augmentation — mirroring upstream's route.tsx/router.tsx `declare module`.
60
+ export type {
61
+ SyncRouteComponent,
62
+ AsyncRouteComponent,
63
+ RouteComponent,
64
+ ErrorRouteComponent,
65
+ NotFoundRouteComponent,
66
+ } from './route';
57
67
  export { routerContext, getRouterContext, matchContext, useRouter } from './context';
58
68
  export { useStore } from './useStore';
59
69
  export { useRouterState } from './useRouterState';
@@ -74,11 +84,13 @@ export {
74
84
  export { useAwaited } from './useAwaited';
75
85
  export { useLinkProps, createLink, linkOptions } from './link';
76
86
  export { useBlocker, Block } from './useBlocker.tsrx';
87
+ export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker.tsrx';
77
88
  export { useMatchRoute, MatchRoute } from './MatchRoute.tsrx';
78
89
  export { useElementScrollRestoration } from './useElementScrollRestoration';
79
90
  export { lazyRouteComponent } from './lazyRouteComponent';
80
91
 
81
92
  export { RouterProvider, RouterContextProvider } from './RouterProvider.tsrx';
93
+ export type { RouterProps } from './RouterProvider.tsrx';
82
94
  export { Outlet } from './Outlet.tsrx';
83
95
  export { Link } from './Link.tsrx';
84
96
  export { Navigate } from './Navigate.tsrx';
@@ -4,18 +4,28 @@
4
4
  // up; the reset key is `not-found-${pathname}-${status}` so navigating away (or a
5
5
  // new load settling) clears the not-found UI.
6
6
  import { isNotFound } from '@tanstack/router-core';
7
+ import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core';
8
+ import type { OctaneNode } from 'octane';
9
+ import type { ErrorInfo } from './route.ts';
7
10
  import { useRouter } from './context.ts';
8
11
  import { useStore } from './useStore.ts';
9
12
  import { CatchBoundary } from './CatchBoundary.tsrx';
10
13
 
11
- function NotFoundFallback(props) {
14
+ function NotFoundFallback(props: {
15
+ error: unknown;
16
+ render?: (error: NotFoundError) => OctaneNode;
17
+ }) {
12
18
  if (isNotFound(props.error)) {
13
19
  return props.render ? props.render(props.error) : null;
14
20
  }
15
21
  throw props.error;
16
22
  }
17
23
 
18
- export function CatchNotFound(props) @{
24
+ export function CatchNotFound(props: {
25
+ fallback?: (error: NotFoundError) => OctaneNode;
26
+ onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
27
+ children: OctaneNode;
28
+ }) @{
19
29
  const router = useRouter();
20
30
  const pathname = useStore(router.stores.location, (l) => l.pathname);
21
31
  const status = useStore(router.stores.status, (s) => s);
@@ -23,14 +33,17 @@ export function CatchNotFound(props) @{
23
33
 
24
34
  <CatchBoundary
25
35
  getResetKey={() => resetKey}
26
- onCatch={(error, errorInfo) => {
36
+ onCatch={(error: Error, errorInfo: ErrorInfo) => {
27
37
  if (isNotFound(error)) {
28
38
  if (props.onCatch) props.onCatch(error, errorInfo);
29
39
  } else {
30
40
  throw error;
31
41
  }
32
42
  }}
33
- errorComponent={(p) => NotFoundFallback({ error: p.error, render: props.fallback })}
43
+ errorComponent={(p: ErrorComponentProps) => NotFoundFallback({
44
+ error: p.error,
45
+ render: props.fallback,
46
+ })}
34
47
  >{props.children}</CatchBoundary>
35
48
  }
36
49
 
@@ -1,7 +1,10 @@
1
1
  // Type declaration for the .tsrx components (resolved by relative path).
2
+ import type { NotFoundError } from '@tanstack/router-core';
3
+ import type { ErrorInfo } from './route';
4
+
2
5
  export declare const CatchNotFound: (props: {
3
- fallback?: (error: any) => unknown;
4
- onCatch?: (error: any, errorInfo: { componentStack: string }) => void;
6
+ fallback?: (error: NotFoundError) => unknown;
7
+ onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
5
8
  children?: unknown;
6
9
  }) => unknown;
7
10
  export declare const DefaultGlobalNotFound: () => unknown;
package/src/route.ts CHANGED
@@ -8,7 +8,9 @@
8
8
  // `withSlot` and passes the call-site symbol as the trailing argument, which
9
9
  // each accessor forwards to the hooks it composes.
10
10
  import { BaseRoute, BaseRootRoute, BaseRouteApi, notFound } from '@tanstack/router-core';
11
+ import type { ErrorComponentProps, NotFoundRouteProps } from '@tanstack/router-core';
11
12
  import { createElement } from 'octane';
13
+ import type { OctaneNode } from 'octane';
12
14
  import {
13
15
  useMatch,
14
16
  useParams,
@@ -22,6 +24,45 @@ import { useRouter } from './context';
22
24
  import { splitSlot, subSlot } from './internal';
23
25
  import { Link } from './Link.tsrx';
24
26
 
27
+ // ── Component types (react-router's route.tsx, on octane renderables) ────────
28
+ // Octane analog of React's `ErrorInfo` (octane reports no component stacks).
29
+ export type ErrorInfo = {
30
+ componentStack?: string | null;
31
+ digest?: string | null;
32
+ };
33
+
34
+ export type SyncRouteComponent<TProps> = (props: TProps) => OctaneNode;
35
+ export type AsyncRouteComponent<TProps> = SyncRouteComponent<TProps> & {
36
+ preload?: () => Promise<void>;
37
+ };
38
+ export type RouteComponent = AsyncRouteComponent<{}>;
39
+ export type ErrorRouteComponent = AsyncRouteComponent<ErrorComponentProps>;
40
+ export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps>;
41
+
42
+ // router-core leaves the framework-facing component options typed `unknown` and
43
+ // each binding narrows them via interface merging — react-router's route.tsx /
44
+ // router.tsx do exactly this with React types; this is the octane equivalent.
45
+ declare module '@tanstack/router-core' {
46
+ interface UpdatableRouteOptionsExtensions {
47
+ component?: RouteComponent;
48
+ errorComponent?: false | null | undefined | ErrorRouteComponent;
49
+ notFoundComponent?: NotFoundRouteComponent;
50
+ pendingComponent?: RouteComponent;
51
+ }
52
+ interface RootRouteOptionsExtensions {
53
+ shellComponent?: (props: { children: OctaneNode }) => OctaneNode;
54
+ }
55
+ interface RouterOptionsExtensions {
56
+ defaultComponent?: RouteComponent;
57
+ defaultErrorComponent?: ErrorRouteComponent;
58
+ defaultPendingComponent?: RouteComponent;
59
+ defaultNotFoundComponent?: NotFoundRouteComponent;
60
+ Wrap?: (props: { children: OctaneNode }) => OctaneNode;
61
+ InnerWrap?: (props: { children: OctaneNode }) => OctaneNode;
62
+ defaultOnCatch?: (error: Error, errorInfo: ErrorInfo) => void;
63
+ }
64
+ }
65
+
25
66
  // Attach the hook accessors to a route-shaped instance (Route / RootRoute /
26
67
  // RouteApi). `strictLoaderHooks: false` is RouteApi's mode — its loader hooks
27
68
  // pass `strict: false` upstream (the api may be read from ancestor layouts).
@@ -7,9 +7,62 @@
7
7
  // in .tsrx so the compiler slots the internal useState/useEffect and callers'
8
8
  // withSlot wrapping keeps per-call-site state independent.
9
9
  import { useState, useEffect, isChildrenBlock } from 'octane';
10
+ import type { OctaneNode } from 'octane';
11
+ import type { BlockerFnArgs, HistoryAction, HistoryLocation } from '@tanstack/history';
10
12
  import { useRouter } from './context.ts';
11
13
 
12
- const IDLE_RESOLVER = {
14
+ // Blocker types, ported from react-router's useBlocker.tsx. Upstream derives the
15
+ // location shapes from the registered route tree's generics; the octane binding's
16
+ // route factories are untyped (`createRoute(options: any)`), so the union of
17
+ // matched-route shapes collapses to the loosely-typed equivalent.
18
+ export interface ShouldBlockFnLocation {
19
+ routeId: string;
20
+ fullPath: string;
21
+ pathname: string;
22
+ params: Record<string, string>;
23
+ search: Record<string, any>;
24
+ }
25
+
26
+ export type BlockerResolver =
27
+ | {
28
+ status: 'blocked';
29
+ current: ShouldBlockFnLocation;
30
+ next: ShouldBlockFnLocation;
31
+ action: HistoryAction;
32
+ proceed: () => void;
33
+ reset: () => void;
34
+ }
35
+ | {
36
+ status: 'idle';
37
+ current: undefined;
38
+ next: undefined;
39
+ action: undefined;
40
+ proceed: undefined;
41
+ reset: undefined;
42
+ };
43
+
44
+ export type ShouldBlockFnArgs = {
45
+ current: ShouldBlockFnLocation;
46
+ next: ShouldBlockFnLocation;
47
+ action: HistoryAction;
48
+ };
49
+
50
+ export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
51
+
52
+ export type UseBlockerOpts = {
53
+ shouldBlockFn: ShouldBlockFn;
54
+ enableBeforeUnload?: boolean | (() => boolean);
55
+ disabled?: boolean;
56
+ withResolver?: boolean;
57
+ };
58
+
59
+ type LegacyBlockerFn = () => Promise<any> | any;
60
+ type LegacyBlockerOpts = {
61
+ blockerFn?: LegacyBlockerFn;
62
+ condition?: boolean | any;
63
+ };
64
+
65
+ const IDLE_RESOLVER: BlockerResolver = {
13
66
  status: 'idle',
14
67
  current: undefined,
15
68
  next: undefined,
@@ -18,7 +71,10 @@ const IDLE_RESOLVER = {
18
71
  reset: undefined,
19
72
  };
20
73
 
21
- function _resolveBlockerOpts(opts, condition) {
74
+ function _resolveBlockerOpts(
75
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
76
+ condition?: boolean | any,
77
+ ): UseBlockerOpts {
22
78
  if (opts === undefined) {
23
79
  return { shouldBlockFn: () => true, withResolver: false };
24
80
  }
@@ -48,18 +104,21 @@ function _resolveBlockerOpts(opts, condition) {
48
104
  };
49
105
  }
50
106
 
51
- export function useBlocker(opts, condition) {
107
+ export function useBlocker(
108
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
109
+ condition?: boolean | any,
110
+ ): BlockerResolver {
52
111
  const { shouldBlockFn, enableBeforeUnload = true, disabled = false, withResolver = false } =
53
112
  _resolveBlockerOpts(opts, condition);
54
113
 
55
114
  const router = useRouter();
56
115
  const { history } = router;
57
116
 
58
- const [resolver, setResolver] = useState(IDLE_RESOLVER);
117
+ const [resolver, setResolver] = useState<BlockerResolver>(IDLE_RESOLVER);
59
118
 
60
119
  useEffect(() => {
61
- const blockerFnComposed = async (blockerFnArgs) => {
62
- function getLocation(location) {
120
+ const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
121
+ function getLocation(location: HistoryLocation): ShouldBlockFnLocation {
63
122
  const parsedLocation = router.parseLocation(location);
64
123
  const matchedRoutes = router.getMatchedRoutes(parsedLocation.pathname);
65
124
  if (matchedRoutes.foundRoute === undefined) {
@@ -95,7 +154,7 @@ export function useBlocker(opts, condition) {
95
154
  if (!withResolver) return shouldBlock;
96
155
  if (!shouldBlock) return false;
97
156
 
98
- const promise = new Promise((resolve) => {
157
+ const promise = new Promise<boolean>((resolve) => {
99
158
  setResolver({
100
159
  status: 'blocked',
101
160
  current,
@@ -119,9 +178,15 @@ export function useBlocker(opts, condition) {
119
178
  return resolver;
120
179
  }
121
180
 
181
+ // Upstream's PromptProps: the blocker options plus optional children (a render
182
+ // prop receiving the resolver, or plain renderables).
183
+ export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
184
+ children?: OctaneNode | ((params: BlockerResolver) => OctaneNode);
185
+ };
186
+
122
187
  // Declarative blocker: registers useBlocker and renders children (optionally a
123
188
  // render prop receiving the resolver).
124
- export function Block(props) @{
189
+ export function Block(props: PromptProps) @{
125
190
  const { children, ...rest } = props;
126
191
  const resolver = useBlocker(rest);
127
192
  const out =
@@ -1,3 +1,59 @@
1
1
  // Type declaration for the .tsrx module (resolved by relative path).
2
- export declare const useBlocker: (opts?: unknown, condition?: unknown) => unknown;
3
- export declare const Block: (props: Record<string, unknown>) => unknown;
2
+ import type { HistoryAction } from '@tanstack/history';
3
+
4
+ export interface ShouldBlockFnLocation {
5
+ routeId: string;
6
+ fullPath: string;
7
+ pathname: string;
8
+ params: Record<string, string>;
9
+ search: Record<string, any>;
10
+ }
11
+
12
+ export type BlockerResolver =
13
+ | {
14
+ status: 'blocked';
15
+ current: ShouldBlockFnLocation;
16
+ next: ShouldBlockFnLocation;
17
+ action: HistoryAction;
18
+ proceed: () => void;
19
+ reset: () => void;
20
+ }
21
+ | {
22
+ status: 'idle';
23
+ current: undefined;
24
+ next: undefined;
25
+ action: undefined;
26
+ proceed: undefined;
27
+ reset: undefined;
28
+ };
29
+
30
+ export type ShouldBlockFnArgs = {
31
+ current: ShouldBlockFnLocation;
32
+ next: ShouldBlockFnLocation;
33
+ action: HistoryAction;
34
+ };
35
+
36
+ export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
37
+
38
+ export type UseBlockerOpts = {
39
+ shouldBlockFn: ShouldBlockFn;
40
+ enableBeforeUnload?: boolean | (() => boolean);
41
+ disabled?: boolean;
42
+ withResolver?: boolean;
43
+ };
44
+
45
+ type LegacyBlockerFn = () => Promise<any> | any;
46
+ type LegacyBlockerOpts = {
47
+ blockerFn?: LegacyBlockerFn;
48
+ condition?: boolean | any;
49
+ };
50
+
51
+ export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
52
+ children?: unknown | ((params: BlockerResolver) => unknown);
53
+ };
54
+
55
+ export declare const useBlocker: (
56
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
57
+ condition?: boolean | any,
58
+ ) => BlockerResolver;
59
+ export declare const Block: (props: PromptProps) => unknown;
package/src/useStore.ts CHANGED
@@ -13,11 +13,22 @@ interface Atom<T> {
13
13
  get: () => T;
14
14
  }
15
15
 
16
- export function useStore<T, S = T>(...args: any[]): S {
16
+ // Public signature mirrors @tanstack/react-store's `useStore(store, selector,
17
+ // compare)`; the store parameter is the structural `{ get }` shape so router-core's
18
+ // `RouterReadableStore`/`RouterWritableStore` atoms (whose types omit `subscribe`)
19
+ // infer `T` directly. The trailing `slot` is the binding's forwarded call-site slot
20
+ // (see internal.ts) — the implementation splits it off the raw argument list.
21
+ export function useStore<T, S = T>(
22
+ atom: { get: () => T },
23
+ selector?: (state: T) => S,
24
+ compare?: (a: S, b: S) => boolean,
25
+ slot?: symbol,
26
+ ): S;
27
+ export function useStore(...args: any[]): any {
17
28
  const [user, slot] = splitSlot(args);
18
- const atom = user[0] as Atom<T>;
19
- const selector = (user[1] ?? ((s: T) => s as unknown as S)) as (s: T) => S;
20
- const compare = (user[2] ?? Object.is) as (a: S, b: S) => boolean;
29
+ const atom = user[0] as Atom<unknown>;
30
+ const selector = (user[1] ?? ((s: unknown) => s)) as (s: unknown) => unknown;
31
+ const compare = (user[2] ?? Object.is) as (a: unknown, b: unknown) => boolean;
21
32
 
22
33
  // Re-subscribe only when the atom identity changes (it's stable across renders).
23
34
  const subscribe = useCallback(
@@ -28,8 +39,8 @@ export function useStore<T, S = T>(...args: any[]): S {
28
39
 
29
40
  // Memoize selector output: same store input → same output; structurally-equal
30
41
  // output keeps its previous reference (so useSyncExternalStore doesn't loop).
31
- const cache = useRef<{ in: T; out: S } | null>(null, subSlot(slot, 'us:cache'));
32
- const getSnapshot = (): S => {
42
+ const cache = useRef<{ in: unknown; out: unknown } | null>(null, subSlot(slot, 'us:cache'));
43
+ const getSnapshot = (): unknown => {
33
44
  const input = atom.get();
34
45
  const prev = cache.current;
35
46
  if (prev && Object.is(prev.in, input)) return prev.out;