@octanejs/tanstack-router 0.2.6 → 0.2.8

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 CHANGED
@@ -134,3 +134,51 @@ package's `status.json`).
134
134
  - **Native DOM events** — link callbacks receive browser events rather than React
135
135
  synthetic events.
136
136
  - Router devtools are distributed separately and are not part of this binding.
137
+
138
+ ## Compiled code routes
139
+
140
+ Use `@octanejs/tanstack-router/compiled` for a tree whose selected route, pending,
141
+ error and not-found components are compiled Octane `@{ ... }` bodies. Its
142
+ `CompiledRouterProvider`, `CompiledOutlet`, and `lazyCompiledRouteComponent`
143
+ avoid retaining the descriptor renderer merely to select a route at runtime.
144
+ TanStack still owns the matching, loading, navigation, history and render
145
+ acknowledgements.
146
+
147
+ ```tsx
148
+ import {
149
+ BaseRootRoute, BaseRoute, createRouter, createBrowserHistory,
150
+ CompiledRouterProvider, CompiledOutlet, lazyCompiledRouteComponent, useParams,
151
+ } from '@octanejs/tanstack-router/compiled';
152
+
153
+ const root = new BaseRootRoute({ component: Layout });
154
+ const item = new BaseRoute({
155
+ getParentRoute: () => root,
156
+ path: 'items/$id',
157
+ component: lazyCompiledRouteComponent(() => import('./Item')),
158
+ });
159
+ const router = createRouter({
160
+ routeTree: root.addChildren([item]),
161
+ history: createBrowserHistory(),
162
+ });
163
+
164
+ function Layout() @{
165
+ <main><CompiledOutlet /></main>
166
+ }
167
+ function App() @{
168
+ <CompiledRouterProvider router={router} />
169
+ }
170
+ ```
171
+
172
+ Import read/navigation hooks (`useMatch`, `useParams`, `useSearch`,
173
+ `useLoaderData`, `useLoaderDeps`, `useRouteContext`, `useLocation`,
174
+ `useMatches`, `useParentMatches`, `useChildMatches`, `useNavigate`,
175
+ `useCanGoBack`, `useRouter`, and `useRouterState`) from the same `/compiled`
176
+ entry. It owns a separate pair of non-callable contexts. Base route factories
177
+ come directly from router-core: use the free hooks in the route components,
178
+ rather than route-bound hooks or `Route.Link` from the normal Octane entry.
179
+
180
+ The first compiled contract excludes `Wrap`, `InnerWrap`, and
181
+ `shellComponent`; the provider reports a diagnostic if they are configured.
182
+ For returned JSX, host strings, renderable wrappers or mixed route trees, retain
183
+ `RouterProvider`, `Outlet` and `lazyRouteComponent` from the ordinary entry.
184
+ SSR and client builds must select the same entry for a tree.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/tanstack-router",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -37,6 +37,7 @@
37
37
  "exports": {
38
38
  ".": "./src/index.ts",
39
39
  "./history": "./src/history.ts",
40
+ "./compiled": "./src/compiled.ts",
40
41
  "./ssr/server": {
41
42
  "types": "./src/ssr/server.ts",
42
43
  "default": "./src/ssr/server.ts"
@@ -58,13 +59,13 @@
58
59
  "isbot": "^5.1.22"
59
60
  },
60
61
  "peerDependencies": {
61
- "octane": "^0.12.0"
62
+ "octane": "^0.13.0"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@tanstack/react-router": "1.170.41",
65
66
  "@tsrx/react": "^0.4.5",
66
67
  "esbuild": "^0.28.1",
67
- "octane": "0.12.0",
68
+ "octane": "0.13.6",
68
69
  "react": "19.2.7",
69
70
  "react-dom": "19.2.7",
70
71
  "vitest": "^4.1.10"
package/src/Matches.tsrx CHANGED
@@ -32,7 +32,7 @@ export function Matches() @{
32
32
  useLayoutEffect(() => {
33
33
  if (router.options.scrollRestoration) setupScrollRestoration(router);
34
34
  }, [router]);
35
- useTransitioner();
35
+ useTransitioner(router);
36
36
 
37
37
  <InnerWrap>
38
38
  <Suspense fallback={pendingElement}>
@@ -19,7 +19,7 @@
19
19
  // restoration, devtools) fire.
20
20
  import { useLayoutEffect, useState, startTransition } from 'octane';
21
21
  import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
22
- import { useRouter } from './context.ts';
22
+ import type { AnyRouter } from '@tanstack/router-core';
23
23
 
24
24
  // Resolve the pending render acknowledgement with whether the expected matches
25
25
  // rendered, then clear the slot. `owner` is `router._rendered`: `[expected, resolve]`.
@@ -29,8 +29,8 @@ export function settleOwner(owner: Array<any>, rendered: boolean) {
29
29
  settle?.(rendered);
30
30
  }
31
31
 
32
- export function useTransitioner() {
33
- const router = useRouter() as any;
32
+ export function useTransitioner(routerInstance: AnyRouter) {
33
+ const router = routerInstance as any;
34
34
  const [, forceRender] = useState<object>();
35
35
 
36
36
  const acknowledgement: Array<any> = (router._rendered ??= []);
@@ -0,0 +1,266 @@
1
+ import {
2
+ CompiledOutlet as CompiledComponent,
3
+ CompiledContext,
4
+ useContext,
5
+ useLayoutEffect,
6
+ useRef,
7
+ } from 'octane';
8
+ import type { NotFoundError } from '@tanstack/router-core';
9
+ import { hasKeys, isNotFound, rootRouteId, setupScrollRestoration } from '@tanstack/router-core';
10
+ import { getScrollRestorationScriptForRouter } from '@tanstack/router-core/scroll-restoration-script';
11
+ import { isServer } from '@tanstack/router-core/isServer';
12
+ import { routerContext, matchContext, useRouter } from './bindings';
13
+ import { useStore } from '../useStore';
14
+ import { useTransitioner, settleOwner } from '../Transitioner.tsrx';
15
+ import { useHydrated } from '../ClientOnly.tsrx';
16
+ import { ErrorComponent } from '../CatchBoundary.tsrx';
17
+ import { createLazyRouteLoader } from '../lazyRouteLoader';
18
+ import type { RouterProps } from '../RouterProvider.tsrx';
19
+
20
+ export function CompiledRouterProvider(props: RouterProps) @{
21
+ const { router, ...rest } = props;
22
+ if (hasKeys(rest)) {
23
+ router.update({
24
+ ...router.options,
25
+ ...rest,
26
+ context: { ...router.options.context, ...rest.context },
27
+ });
28
+ }
29
+ if (router.options.Wrap || router.options.InnerWrap) {
30
+ throw new Error(
31
+ 'CompiledRouterProvider does not accept Wrap or InnerWrap. Use RouterProvider for renderable wrappers.',
32
+ );
33
+ }
34
+ <CompiledContext context={routerContext} value={router}>
35
+ <CompiledMatches />
36
+ </CompiledContext>
37
+ }
38
+
39
+ function CompiledMatches() @{
40
+ const router = useRouter();
41
+ const rootRoute = (router.routesById as any)[rootRouteId];
42
+ useLayoutEffect(() => {
43
+ if (router.options.scrollRestoration) setupScrollRestoration(router);
44
+ }, [router]);
45
+ useTransitioner(router);
46
+ @try {
47
+ <CompiledMatchesInner />
48
+ } @pending {
49
+ <CompiledPending route={rootRoute} />
50
+ } @catch (error, reset) {
51
+ if (router.options.disableGlobalCatchBoundary) throw error;
52
+ <CompiledFailure error={error} reset={reset} route={rootRoute} global={true} />
53
+ }
54
+ }
55
+
56
+ function CompiledMatchesInner() @{
57
+ const router = useRouter();
58
+ const acknowledgement: Array<any> = ((router as any)._rendered ??= []);
59
+ const matches = useStore(router.stores.matches, (m: any) => acknowledgement[0] ?? m);
60
+ const rootMatch = matches[0];
61
+ useLayoutEffect(() => {
62
+ if (acknowledgement[0] === matches) settleOwner(acknowledgement, true);
63
+ }, [acknowledgement, matches]);
64
+ @if (rootMatch) {
65
+ <CompiledMatch routeId={rootMatch.routeId} />
66
+ }
67
+ }
68
+
69
+ function CompiledMatch(props: { routeId: string }) @{
70
+ const router = useRouter();
71
+ const store = router.stores.getMatchStore(props.routeId);
72
+ const ssr = useStore(store, (match: any) => match.ssr);
73
+ const route = (router.routesById as Record<string, any>)[props.routeId];
74
+ if (route.isRoot && route.options.shellComponent) {
75
+ throw new Error(
76
+ 'CompiledRouterProvider does not accept shellComponent. Use RouterProvider for renderable document shells.',
77
+ );
78
+ }
79
+ const pending = route.options.pendingComponent ?? router.options.defaultPendingComponent;
80
+ const errorComponent = route.options.errorComponent ?? router.options.defaultErrorComponent;
81
+ const noSsr = ssr === false || ssr === 'data-only';
82
+ const hasSuspense =
83
+ (!route.isRoot || route.options.wrapInSuspense || noSsr) &&
84
+ ((route.options.wrapInSuspense ?? pending ?? errorComponent?.preload) || noSsr);
85
+ <CompiledContext context={matchContext} value={props.routeId}>
86
+ @if (hasSuspense) {
87
+ @try {
88
+ <CompiledMatchContent route={route} routeId={props.routeId} noSsr={noSsr} />
89
+ } @pending {
90
+ <CompiledPending route={route} />
91
+ }
92
+ } @else {
93
+ <CompiledMatchContent route={route} routeId={props.routeId} noSsr={noSsr} />
94
+ }
95
+ @if (route.parentRoute?.id === rootRouteId && router.options.scrollRestoration) {
96
+ <CompiledScrollRestoration />
97
+ }
98
+ </CompiledContext>
99
+ }
100
+
101
+ // Without a configured Suspense boundary, both lazy imports and loader promises
102
+ // pass through to the next ancestor boundary. The match still owns its errors.
103
+ function CompiledMatchContent(props: { route: any; routeId: string; noSsr: boolean }) @{
104
+ @try {
105
+ <CompiledMatchInner routeId={props.routeId} noSsr={props.noSsr} />
106
+ } @catch (error, reset) {
107
+ <CompiledFailure error={error} reset={reset} route={props.route} />
108
+ }
109
+ }
110
+
111
+ function CompiledMatchInner(props: { routeId: string; noSsr: boolean }) @{
112
+ const router = useRouter();
113
+ const match = useStore(router.stores.getMatchStore(props.routeId), (m: any) => m);
114
+ const route = (router.routesById as Record<string, any>)[props.routeId];
115
+ const hydrated = useHydrated();
116
+ const Comp = route.options.component ?? router.options.defaultComponent;
117
+ const remountFn = route.options.remountDeps ?? router.options.defaultRemountDeps;
118
+ const remountDeps = remountFn?.({
119
+ routeId: props.routeId,
120
+ loaderDeps: match.loaderDeps,
121
+ params: match._strictParams,
122
+ search: match._strictSearch,
123
+ });
124
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined;
125
+ if (match.status === 'pending' && !props.noSsr && (router as any)._tx) {
126
+ throw (router as any)._tx[5];
127
+ }
128
+ @if ((props.noSsr && !hydrated) || match.status === 'pending') {
129
+ <CompiledPending route={route} />
130
+ } @else if (match.status === 'notFound') {
131
+ <CompiledNotFound route={route} error={match.error} />
132
+ } @else if (match.status === 'error') {
133
+ @if (isServer ?? (router as any).isServer) {
134
+ <CompiledComponent
135
+ component={
136
+ route.options.errorComponent ?? router.options.defaultErrorComponent ?? ErrorComponent
137
+ }
138
+ props={{ error: match.error, reset: undefined, info: { componentStack: '' } }}
139
+ />
140
+ } @else {
141
+ throw match.error;
142
+ <></>
143
+ }
144
+ } @else if (Comp) {
145
+ <CompiledComponent key={key} component={Comp} props={{}} />
146
+ } @else {
147
+ <CompiledOutlet />
148
+ }
149
+ }
150
+
151
+ // Reads exactly the next TanStack match, preserving nearest-match contexts and
152
+ // fuzzy/root not-found placement. This is distinct from octane's component outlet.
153
+ export function CompiledOutlet() @{
154
+ const router = useRouter();
155
+ const routeId = useContext(matchContext)!;
156
+ const store = router.stores.getMatchStore(routeId);
157
+ const notFound = useStore(
158
+ store,
159
+ (m: any) => ({ global: !!m?._notFound, error: m?.error }),
160
+ (a, b) => a.global === b.global && a.error === b.error,
161
+ );
162
+ const childId = useStore(router.stores.ids, (ids: Array<string>) => {
163
+ const i = ids.indexOf(routeId);
164
+ return i >= 0 ? ids[i + 1] : undefined;
165
+ });
166
+ @if (notFound.global) {
167
+ <CompiledNotFound route={(router.routesById as any)[routeId]} error={notFound.error} />
168
+ } @else if (childId) {
169
+ @if (routeId === rootRouteId) {
170
+ @try {
171
+ <CompiledMatch routeId={childId} />
172
+ } @pending {
173
+ <CompiledPending />
174
+ }
175
+ } @else {
176
+ <CompiledMatch routeId={childId} />
177
+ }
178
+ }
179
+ }
180
+
181
+ function CompiledPending(props: { route?: any }) @{
182
+ const router = useRouter();
183
+ const Pending = props.route?.options.pendingComponent ?? router.options.defaultPendingComponent;
184
+ @if (Pending) {
185
+ <CompiledComponent component={Pending} props={{}} />
186
+ }
187
+ }
188
+
189
+ function CompiledNotFound(props: { route: any; error?: NotFoundError }) @{
190
+ const router = useRouter();
191
+ const NotFound = props.route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
192
+ @if (NotFound) {
193
+ <CompiledComponent component={NotFound} props={props.error ?? {}} />
194
+ } @else {
195
+ <p>Not Found</p>
196
+ }
197
+ }
198
+
199
+ function CompiledFailure(props: { error: any; reset: () => void; route: any; global?: boolean }) @{
200
+ const router = useRouter();
201
+ const errorComponent = props.global
202
+ ? ErrorComponent
203
+ : (props.route.options.errorComponent ?? router.options.defaultErrorComponent);
204
+ const notFoundComponent = props.route.isRoot
205
+ ? (props.route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component)
206
+ : props.route.options.notFoundComponent;
207
+ const isMissing = isNotFound(props.error);
208
+ if (isMissing) {
209
+ const missing = props.error as NotFoundError;
210
+ missing.routeId ??= props.route.id;
211
+ if (!notFoundComponent || missing.routeId !== props.route.id) throw missing;
212
+ } else if (!errorComponent) {
213
+ throw props.error;
214
+ }
215
+
216
+ // Report an error only at the boundary that owns it and retry after its
217
+ // TanStack match identity changes, just like the normal binding.
218
+ const reported = useRef<unknown>(null);
219
+ if (!isMissing && !props.global && reported.current !== props.error) {
220
+ reported.current = props.error;
221
+ const onCatch = props.route.options.onCatch ?? router.options.defaultOnCatch;
222
+ onCatch?.(props.error, { componentStack: '' });
223
+ }
224
+ const store = router.stores.getMatchStore(props.route.id);
225
+ const match = useStore(store, (m: any) => m);
226
+ const firstMatch = useRef(match);
227
+ useLayoutEffect(() => {
228
+ if (firstMatch.current !== match) {
229
+ firstMatch.current = match;
230
+ props.reset();
231
+ }
232
+ }, [match]);
233
+
234
+ @if (isMissing) {
235
+ <CompiledComponent component={notFoundComponent} props={props.error} />
236
+ } @else {
237
+ <CompiledComponent
238
+ component={errorComponent}
239
+ props={{ error: props.error, reset: props.reset, info: { componentStack: '' } }}
240
+ />
241
+ }
242
+ }
243
+
244
+ function CompiledScrollRestoration() @{
245
+ const router = useRouter();
246
+ const hydrated = useHydrated();
247
+ const option = router.options.scrollRestoration;
248
+ const script = getScrollRestorationScriptForRouter(router);
249
+ const rendersScript = typeof option !== 'function' || option({ location: router.latestLocation });
250
+ @if ((script !== null || rendersScript) && ((isServer ?? router.isServer) || !hydrated)) {
251
+ <script
252
+ nonce={router.options.ssr?.nonce}
253
+ suppressHydrationWarning
254
+ dangerouslySetInnerHTML={{ __html: script ?? '' }}
255
+ />
256
+ }
257
+ }
258
+
259
+ export function lazyCompiledRouteComponent(importer: () => Promise<any>, exportName?: string) {
260
+ const loader = createLazyRouteLoader(importer, exportName);
261
+ function Lazy(props: any) @{
262
+ <CompiledComponent component={loader.read()} props={props} />
263
+ }
264
+ Lazy.preload = loader.preload;
265
+ return Lazy;
266
+ }
@@ -0,0 +1,40 @@
1
+ import { createCompiledContext, useContext } from 'octane';
2
+ import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
3
+ import { createRouterHooks } from '../hookBindings';
4
+ import { createUseRouterState } from '../routerStateBinding';
5
+
6
+ // The normal contexts remain callable providers. These contexts belong only to
7
+ // the opt-in compiled tree so that using it never imports a generic provider.
8
+ export const routerContext = createCompiledContext<AnyRouter | undefined>(undefined);
9
+ export const matchContext = createCompiledContext<string | undefined>(undefined);
10
+
11
+ export function useRouter<TRouter extends AnyRouter = RegisteredRouter>(opts?: {
12
+ router?: TRouter;
13
+ warn?: boolean;
14
+ }): TRouter;
15
+ export function useRouter(...args: unknown[]): AnyRouter {
16
+ const opts = (args.length && typeof args[0] !== 'symbol' ? args[0] : undefined) as
17
+ { router?: AnyRouter; warn?: boolean } | undefined;
18
+ const router = opts?.router ?? useContext(routerContext);
19
+ if (!router && opts?.warn !== false) {
20
+ throw new Error('Use compiled router hooks inside <CompiledRouterProvider>.');
21
+ }
22
+ return router as AnyRouter;
23
+ }
24
+
25
+ export const {
26
+ useMatch,
27
+ useParams,
28
+ useSearch,
29
+ useLoaderData,
30
+ useLoaderDeps,
31
+ useRouteContext,
32
+ useLocation,
33
+ useMatches,
34
+ useParentMatches,
35
+ useChildMatches,
36
+ useNavigate,
37
+ useCanGoBack,
38
+ } = createRouterHooks(useRouter, () => useContext(matchContext));
39
+
40
+ export const useRouterState = createUseRouterState(useRouter);
@@ -0,0 +1,32 @@
1
+ // Opt-in source-owned renderer for code routes with compiled bodies only.
2
+ import './frameworkTypes';
3
+ export * from '@tanstack/router-core';
4
+ export { Router, createRouter } from './router';
5
+ export {
6
+ createHistory,
7
+ createBrowserHistory,
8
+ createHashHistory,
9
+ createMemoryHistory,
10
+ } from './history';
11
+ export {
12
+ useRouter,
13
+ useRouterState,
14
+ useMatch,
15
+ useParams,
16
+ useSearch,
17
+ useLoaderData,
18
+ useLoaderDeps,
19
+ useRouteContext,
20
+ useLocation,
21
+ useMatches,
22
+ useParentMatches,
23
+ useChildMatches,
24
+ useNavigate,
25
+ useCanGoBack,
26
+ } from './compiled/bindings';
27
+ export type { UseRouterStateOptions, UseRouterStateResult } from './routerStateBinding';
28
+ export {
29
+ CompiledRouterProvider,
30
+ CompiledOutlet,
31
+ lazyCompiledRouteComponent,
32
+ } from './compiled/Matches.tsrx';
package/src/fileRoute.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createRoute } from './route';
2
+ import type { Route } from './route';
2
3
  import {
3
4
  useLoaderData,
4
5
  useLoaderDeps,
@@ -22,7 +23,6 @@ import type {
22
23
  Register,
23
24
  RegisteredRouter,
24
25
  ResolveParams,
25
- Route,
26
26
  RouteById,
27
27
  RouteConstraints,
28
28
  RouteIds,
@@ -268,7 +268,8 @@ export function createLazyRoute<
268
268
 
269
269
  export function createLazyFileRoute<
270
270
  TFilePath extends keyof FileRoutesByPath,
271
- TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
271
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'] & AnyRoute =
272
+ FileRoutesByPath[TFilePath]['preLoaderRoute'],
272
273
  >(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute<TRoute> {
273
274
  if (typeof id === 'object') {
274
275
  return new LazyRoute<TRoute>(id) as any;
@@ -30,13 +30,16 @@ declare module '@tanstack/router-core' {
30
30
  }
31
31
 
32
32
  interface RouteExtensions<in out TId extends string, in out TFullPath extends string> {
33
- useMatch: UseMatchRoute<TId>;
34
- useRouteContext: UseRouteContextRoute<TId>;
35
- useSearch: UseSearchRoute<TId>;
36
- useParams: UseParamsRoute<TId>;
37
- useLoaderDeps: UseLoaderDepsRoute<TId>;
38
- useLoaderData: UseLoaderDataRoute<TId>;
39
- useNavigate: () => UseNavigateResult<TFullPath>;
40
- Link: LinkComponentRoute<TFullPath>;
33
+ // Core base routes are also valid in compiled trees and do not own bound
34
+ // renderer hooks. The ordinary binding's concrete Route classes provide
35
+ // each of these members as required properties.
36
+ useMatch?: UseMatchRoute<TId>;
37
+ useRouteContext?: UseRouteContextRoute<TId>;
38
+ useSearch?: UseSearchRoute<TId>;
39
+ useParams?: UseParamsRoute<TId>;
40
+ useLoaderDeps?: UseLoaderDepsRoute<TId>;
41
+ useLoaderData?: UseLoaderDataRoute<TId>;
42
+ useNavigate?: () => UseNavigateResult<TFullPath>;
43
+ Link?: LinkComponentRoute<TFullPath>;
41
44
  }
42
45
  }