@octanejs/tanstack-router 0.2.9 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +23 -7
  2. package/package.json +4 -3
  3. package/src/Asset.tsrx +1 -1
  4. package/src/Await.tsrx +1 -1
  5. package/src/CatchBoundary.tsrx +11 -41
  6. package/src/ClientOnly.tsrx +1 -1
  7. package/src/ErrorComponent.tsrx +38 -0
  8. package/src/HeadContent.tsrx +4 -2
  9. package/src/Link.tsrx +3 -1
  10. package/src/Link.tsrx.d.ts +3 -0
  11. package/src/Match.tsrx +3 -3
  12. package/src/MatchRoute.tsrx +5 -5
  13. package/src/Matches.tsrx +6 -19
  14. package/src/Navigate.tsrx +1 -1
  15. package/src/Outlet.tsrx +2 -2
  16. package/src/RouteNotFound.tsrx +1 -1
  17. package/src/RouterProvider.tsrx +1 -1
  18. package/src/ScriptOnce.tsrx +14 -15
  19. package/src/Scripts.tsrx +1 -1
  20. package/src/ScrollRestoration.tsrx +1 -1
  21. package/src/Transitioner.tsrx +80 -62
  22. package/src/assetComponent.tsrx +1 -1
  23. package/src/compiled/Document.tsrx +2 -0
  24. package/src/compiled/Link.tsrx +1 -3
  25. package/src/compiled/Matches.tsrx +128 -120
  26. package/src/compiled/RouterProvider.tsrx +27 -0
  27. package/src/compiled.ts +6 -3
  28. package/src/core.ts +8 -0
  29. package/src/externalHydration.ts +2 -2
  30. package/src/fileRoute.ts +1 -1
  31. package/src/headContentBinding.ts +10 -15
  32. package/src/history.ts +16 -8
  33. package/src/historyState.ts +37 -0
  34. package/src/hookBindings.ts +2 -0
  35. package/src/index.ts +4 -4
  36. package/src/{link.ts → linkApi.ts} +1 -3
  37. package/src/linkBinding.ts +14 -10
  38. package/src/linkComponent.tsrx +3 -3
  39. package/src/not-found.tsrx +4 -4
  40. package/src/route.ts +2 -1
  41. package/src/router.ts +5 -0
  42. package/src/routerStateBinding.ts +2 -0
  43. package/src/scriptComponent.tsrx +22 -15
  44. package/src/scroll-restoration.tsrx +1 -1
  45. package/src/ssr/RouterClient.tsrx +1 -1
  46. package/src/ssr/renderRouterToStream.ts +13 -4
  47. package/src/useBlocker.tsrx +1 -1
  48. package/src/useStore.ts +2 -0
@@ -3,10 +3,8 @@
3
3
  import { createUseLinkProps } from '../linkBinding';
4
4
  import { createLinkComponent } from '../linkComponent.tsrx';
5
5
  import { useRouter } from './bindings';
6
- import type { createLink as StandardCreateLink } from '../link';
6
+ import type { createLink as StandardCreateLink } from '../linkApi';
7
7
 
8
- // The shared hook intentionally retains flushSync's synchronous navigation cue.
9
- // The compiled entry does not relax its observable event timing.
10
8
  export const useLinkProps = createUseLinkProps(useRouter);
11
9
  export const Link = createLinkComponent(useLinkProps);
12
10
 
@@ -1,34 +1,31 @@
1
+ 'use strong';
2
+
1
3
  import {
4
+ __runOnCatch,
2
5
  CompiledOutlet as CompiledComponent,
3
6
  CompiledContext,
7
+ Hydrate,
8
+ trustHTML,
4
9
  useContext,
5
10
  useLayoutEffect,
6
11
  useRef,
7
12
  } 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';
13
+ import { never } from 'octane/hydration';
14
+ import type { AnyRouter, NotFoundError } from '@tanstack/router-core';
15
+ import { isNotFound, rootRouteId, setupScrollRestoration } from '@tanstack/router-core';
11
16
  import { isServer } from '@tanstack/router-core/isServer';
17
+ import { getScrollRestorationScriptForRouter } from '@tanstack/router-core/scroll-restoration-script';
12
18
  import { routerContext, matchContext, useRouter } from './bindings';
13
19
  import { useStore } from '../useStore';
14
- import { useTransitioner, settleOwner } from '../Transitioner.tsrx';
20
+ import { useTransitioner, usePresentedMatches } from '../Transitioner.tsrx';
15
21
  import { useHydrated } from '../ClientOnly.tsrx';
16
- import { ErrorComponent } from '../CatchBoundary.tsrx';
17
- import { createLazyRouteLoader } from '../lazyRouteLoader';
18
- import type { RouterProps } from '../RouterProvider.tsrx';
22
+ import { ErrorComponent } from '../ErrorComponent.tsrx';
19
23
 
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
- }
24
+ export function PreconfiguredRouterProvider(props: { router: AnyRouter }) @{
25
+ const router = props.router;
29
26
  if (router.options.Wrap || router.options.InnerWrap) {
30
27
  throw new Error(
31
- 'CompiledRouterProvider does not accept Wrap or InnerWrap. Use RouterProvider for renderable wrappers.',
28
+ 'Compiled providers do not accept Wrap or InnerWrap. Use RouterProvider from @octanejs/tanstack-router for renderable wrappers.',
32
29
  );
33
30
  }
34
31
  <CompiledContext context={routerContext} value={router}>
@@ -36,41 +33,36 @@ export function CompiledRouterProvider(props: RouterProps) @{
36
33
  </CompiledContext>
37
34
  }
38
35
 
39
- function CompiledWrap(props: { component?: any; children?: unknown }) @{
40
- @if (props.component) {
41
- <CompiledComponent component={props.component} props={{ children: props.children }} />
42
- } @else {
43
- <>{props.children}</>
44
- }
36
+ function setupRouterScrollRestoration(router: AnyRouter) {
37
+ if (router.options.scrollRestoration) setupScrollRestoration(router);
45
38
  }
46
39
 
47
40
  function CompiledMatches() @{
48
41
  const router = useRouter();
49
42
  const rootRoute = (router.routesById as any)[rootRouteId];
50
- useLayoutEffect(() => {
51
- if (router.options.scrollRestoration) setupScrollRestoration(router);
52
- }, [router]);
53
- const expectedMatches = useTransitioner(router);
43
+ useLayoutEffect(() => setupRouterScrollRestoration(router));
44
+ useTransitioner(router);
54
45
  @try {
55
- <CompiledMatchesInner expectedMatches={expectedMatches} />
46
+ <CompiledMatchesInner />
56
47
  } @pending {
57
48
  <CompiledPending route={rootRoute} />
58
49
  } @catch (error, reset) {
59
50
  if (router.options.disableGlobalCatchBoundary) throw error;
60
- <CompiledFailure error={error} reset={reset} route={rootRoute} global={true} />
51
+ const failure = resolveFailure(error, rootRoute, router, true);
52
+ <CompiledFailure
53
+ error={error}
54
+ reset={reset}
55
+ route={rootRoute}
56
+ router={router}
57
+ failure={failure}
58
+ />
61
59
  }
62
60
  }
63
61
 
64
- function CompiledMatchesInner(props: { expectedMatches: unknown }) @{
62
+ function CompiledMatchesInner() @{
65
63
  const router = useRouter();
66
- const acknowledgement: Array<any> = ((router as any)._rendered ??= []);
67
- const matches = useStore(router.stores.matches, (m: any) =>
68
- acknowledgement[0] === props.expectedMatches ? (props.expectedMatches ?? m) : m,
69
- ) as any;
64
+ const matches = usePresentedMatches(router);
70
65
  const rootMatch = matches[0];
71
- useLayoutEffect(() => {
72
- if (acknowledgement[0] === matches) settleOwner(acknowledgement, true);
73
- }, [acknowledgement, matches]);
74
66
  @if (rootMatch) {
75
67
  <CompiledContext context={matchContext} value={rootMatch.routeId}>
76
68
  <CompiledMatch routeId={rootMatch.routeId} />
@@ -78,7 +70,31 @@ function CompiledMatchesInner(props: { expectedMatches: unknown }) @{
78
70
  }
79
71
  }
80
72
 
81
- function CompiledMatch(props: { routeId: string }) @{
73
+ function CompiledPending(props: { route?: any; waitForTransition?: boolean }) @{
74
+ const router = useRouter();
75
+ if (props.waitForTransition) {
76
+ // Observe the transaction with the pending match it owns, not through the
77
+ // stable router identity during Strong render.
78
+ const transition = useStore(router.stores.getMatchStore(props.route.id), () => router._tx?.[5]);
79
+ if (transition) throw transition;
80
+ }
81
+ const Pending = props.route?.options.pendingComponent ?? router.options.defaultPendingComponent;
82
+ @if (Pending) {
83
+ <CompiledComponent component={Pending} props={{}} />
84
+ }
85
+ }
86
+
87
+ // Route IDs enter this tree from published match snapshots. Router-core has
88
+ // already created their stable presentation stores before notifying readers.
89
+ function CompiledWrap(props: { component?: any; children?: unknown }) @{
90
+ @if (props.component) {
91
+ <CompiledComponent component={props.component} props={{ children: props.children }} />
92
+ } @else {
93
+ <>{props.children}</>
94
+ }
95
+ }
96
+
97
+ export function CompiledMatch(props: { routeId: string }) @{
82
98
  const router = useRouter();
83
99
  const store = router.stores.getMatchStore(props.routeId);
84
100
  const ssr = useStore(store, (match: any) => match.ssr);
@@ -107,16 +123,88 @@ function CompiledMatch(props: { routeId: string }) @{
107
123
  </CompiledWrap>
108
124
  }
109
125
 
126
+ function CompiledScrollRestoration() @{
127
+ const router = useRouter();
128
+ const hydrated = useHydrated();
129
+ const option = router.options.scrollRestoration;
130
+ const script = getScrollRestorationScriptForRouter(router);
131
+ // Keep the location predicate live: the router identity survives location changes.
132
+ let rendersScript = true;
133
+ if (typeof option === 'function') rendersScript = option({ location: router.latestLocation });
134
+ @if ((script !== null || rendersScript) && ((isServer ?? router.isServer) || !hydrated)) {
135
+ <Hydrate split={false} when={never()}>
136
+ <script nonce={router.options.ssr?.nonce} dangerouslySetInnerHTML={trustHTML(script ?? '')} />
137
+ </Hydrate>
138
+ }
139
+ }
140
+
110
141
  // Without a configured Suspense boundary, both lazy imports and loader promises
111
142
  // pass through to the next ancestor boundary. The match still owns its errors.
112
143
  function CompiledMatchContent(props: { route: any; routeId: string; noSsr: boolean }) @{
113
144
  @try {
114
145
  <CompiledMatchInner routeId={props.routeId} noSsr={props.noSsr} />
115
146
  } @catch (error, reset) {
116
- <CompiledFailure error={error} reset={reset} route={props.route} />
147
+ const router = useRouter();
148
+ const failure = resolveFailure(error, props.route, router, false);
149
+ <CompiledFailure
150
+ error={error}
151
+ reset={reset}
152
+ route={props.route}
153
+ router={router}
154
+ failure={failure}
155
+ />
117
156
  }
118
157
  }
119
158
 
159
+ function resolveFailure(error: any, route: any, router: AnyRouter, global: boolean) {
160
+ const errorComponent = global
161
+ ? ErrorComponent
162
+ : (route.options.errorComponent ?? router.options.defaultErrorComponent);
163
+ const notFoundComponent = route.isRoot
164
+ ? (route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component)
165
+ : route.options.notFoundComponent;
166
+ const missing = isNotFound(error);
167
+ if (missing) {
168
+ error.routeId ??= route.id;
169
+ if (!notFoundComponent || error.routeId !== route.id) throw error;
170
+ } else {
171
+ if (!errorComponent) throw error;
172
+ // The compatibility owner initializes its reported-error ref to null.
173
+ if (!global && error !== null) {
174
+ __runOnCatch(() => {
175
+ const onCatch = route.options.onCatch ?? router.options.defaultOnCatch;
176
+ onCatch?.(error, { componentStack: '' });
177
+ });
178
+ }
179
+ }
180
+ return { component: missing ? notFoundComponent : errorComponent, missing };
181
+ }
182
+
183
+ function CompiledFailure(props: {
184
+ error: any;
185
+ reset: () => void;
186
+ route: any;
187
+ router: AnyRouter;
188
+ failure: ReturnType<typeof resolveFailure>;
189
+ }) @{
190
+ const match = useStore(props.router.stores.getMatchStore(props.route.id), (value: any) => value);
191
+ const firstMatch = useRef(match);
192
+ useLayoutEffect(() => {
193
+ if (firstMatch.current !== match) {
194
+ firstMatch.current = match;
195
+ props.reset();
196
+ }
197
+ });
198
+ <CompiledComponent
199
+ component={props.failure.component}
200
+ props={
201
+ props.failure.missing
202
+ ? props.error
203
+ : { error: props.error, reset: props.reset, info: { componentStack: '' } }
204
+ }
205
+ />
206
+ }
207
+
120
208
  function CompiledMatchInner(props: { routeId: string; noSsr: boolean }) @{
121
209
  const router = useRouter();
122
210
  const match = useStore(router.stores.getMatchStore(props.routeId), (m: any) => m);
@@ -131,11 +219,8 @@ function CompiledMatchInner(props: { routeId: string; noSsr: boolean }) @{
131
219
  search: match._strictSearch,
132
220
  });
133
221
  const key = remountDeps ? JSON.stringify(remountDeps) : undefined;
134
- if (match.status === 'pending' && !props.noSsr && (router as any)._tx) {
135
- throw (router as any)._tx[5];
136
- }
137
222
  @if ((props.noSsr && !hydrated) || match.status === 'pending') {
138
- <CompiledPending route={route} />
223
+ <CompiledPending route={route} waitForTransition={match.status === 'pending' && !props.noSsr} />
139
224
  } @else if (match.status === 'notFound') {
140
225
  <CompiledNotFound route={route} error={match.error} />
141
226
  } @else if (match.status === 'error') {
@@ -187,14 +272,6 @@ export function CompiledOutlet() @{
187
272
  }
188
273
  }
189
274
 
190
- function CompiledPending(props: { route?: any }) @{
191
- const router = useRouter();
192
- const Pending = props.route?.options.pendingComponent ?? router.options.defaultPendingComponent;
193
- @if (Pending) {
194
- <CompiledComponent component={Pending} props={{}} />
195
- }
196
- }
197
-
198
275
  function CompiledNotFound(props: { route: any; error?: NotFoundError }) @{
199
276
  const router = useRouter();
200
277
  const NotFound = props.route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
@@ -204,72 +281,3 @@ function CompiledNotFound(props: { route: any; error?: NotFoundError }) @{
204
281
  <p>Not Found</p>
205
282
  }
206
283
  }
207
-
208
- function CompiledFailure(props: { error: any; reset: () => void; route: any; global?: boolean }) @{
209
- const router = useRouter();
210
- const errorComponent = props.global
211
- ? ErrorComponent
212
- : (props.route.options.errorComponent ?? router.options.defaultErrorComponent);
213
- const notFoundComponent = props.route.isRoot
214
- ? (props.route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component)
215
- : props.route.options.notFoundComponent;
216
- const isMissing = isNotFound(props.error);
217
- if (isMissing) {
218
- const missing = props.error as NotFoundError;
219
- missing.routeId ??= props.route.id;
220
- if (!notFoundComponent || missing.routeId !== props.route.id) throw missing;
221
- } else if (!errorComponent) {
222
- throw props.error;
223
- }
224
-
225
- // Report an error only at the boundary that owns it and retry after its
226
- // TanStack match identity changes, just like the normal binding.
227
- const reported = useRef<unknown>(null);
228
- if (!isMissing && !props.global && reported.current !== props.error) {
229
- reported.current = props.error;
230
- const onCatch = props.route.options.onCatch ?? router.options.defaultOnCatch;
231
- onCatch?.(props.error, { componentStack: '' });
232
- }
233
- const store = router.stores.getMatchStore(props.route.id);
234
- const match = useStore(store, (m: any) => m);
235
- const firstMatch = useRef(match);
236
- useLayoutEffect(() => {
237
- if (firstMatch.current !== match) {
238
- firstMatch.current = match;
239
- props.reset();
240
- }
241
- }, [match]);
242
-
243
- @if (isMissing) {
244
- <CompiledComponent component={notFoundComponent} props={props.error} />
245
- } @else {
246
- <CompiledComponent
247
- component={errorComponent}
248
- props={{ error: props.error, reset: props.reset, info: { componentStack: '' } }}
249
- />
250
- }
251
- }
252
-
253
- function CompiledScrollRestoration() @{
254
- const router = useRouter();
255
- const hydrated = useHydrated();
256
- const option = router.options.scrollRestoration;
257
- const script = getScrollRestorationScriptForRouter(router);
258
- const rendersScript = typeof option !== 'function' || option({ location: router.latestLocation });
259
- @if ((script !== null || rendersScript) && ((isServer ?? router.isServer) || !hydrated)) {
260
- <script
261
- nonce={router.options.ssr?.nonce}
262
- suppressHydrationWarning
263
- dangerouslySetInnerHTML={{ __html: script ?? '' }}
264
- />
265
- }
266
- }
267
-
268
- export function lazyCompiledRouteComponent(importer: () => Promise<any>, exportName?: string) {
269
- const loader = createLazyRouteLoader(importer, exportName);
270
- function Lazy(props: any) @{
271
- <CompiledComponent component={loader.read()} props={props} />
272
- }
273
- Lazy.preload = loader.preload;
274
- return Lazy;
275
- }
@@ -0,0 +1,27 @@
1
+ import { CompiledOutlet as CompiledComponent } from 'octane';
2
+ import { hasKeys } from '@tanstack/router-core';
3
+ import { createLazyRouteLoader } from '../lazyRouteLoader';
4
+ import type { RouterProps } from '../RouterProvider.tsrx';
5
+ import { PreconfiguredRouterProvider } from './Matches.tsrx';
6
+
7
+ export function CompiledRouterProvider(props: RouterProps) @{
8
+ const { router, ...rest } = props;
9
+ // Option props must apply before descendants render, including during SSR.
10
+ if (hasKeys(rest)) {
11
+ router.update({
12
+ ...router.options,
13
+ ...rest,
14
+ context: { ...router.options.context, ...rest.context },
15
+ });
16
+ }
17
+ <PreconfiguredRouterProvider router={router} />
18
+ }
19
+
20
+ export function lazyCompiledRouteComponent(importer: () => Promise<any>, exportName?: string) {
21
+ const loader = createLazyRouteLoader(importer, exportName);
22
+ function Lazy(props: any) @{
23
+ <CompiledComponent component={loader.read()} props={props} />
24
+ }
25
+ Lazy.preload = loader.preload;
26
+ return Lazy;
27
+ }
package/src/compiled.ts CHANGED
@@ -37,13 +37,16 @@ export {
37
37
  } from './compiled/bindings';
38
38
  export type { UseRouterStateOptions, UseRouterStateResult } from './routerStateBinding';
39
39
  export {
40
- CompiledRouterProvider,
41
- CompiledRouterProvider as RouterProvider,
42
40
  CompiledOutlet,
43
41
  CompiledOutlet as Outlet,
42
+ PreconfiguredRouterProvider,
43
+ } from './compiled/Matches.tsrx';
44
+ export {
45
+ CompiledRouterProvider,
46
+ CompiledRouterProvider as RouterProvider,
44
47
  lazyCompiledRouteComponent,
45
48
  lazyCompiledRouteComponent as lazyRouteComponent,
46
- } from './compiled/Matches.tsrx';
49
+ } from './compiled/RouterProvider.tsrx';
47
50
  export {
48
51
  createRoute,
49
52
  createRootRoute,
package/src/core.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from '@tanstack/router-core';
2
+ export { Router, createRouter } from './router';
3
+ export {
4
+ createHistory,
5
+ createBrowserHistory,
6
+ createHashHistory,
7
+ createMemoryHistory,
8
+ } from './history';
@@ -2,8 +2,8 @@ const EXTERNAL_HYDRATION_PROMISE = Symbol.for('octane.external-hydration-promise
2
2
 
3
3
  type ExternalHydrationThenable<T> = PromiseLike<T> & {
4
4
  [EXTERNAL_HYDRATION_PROMISE]: true;
5
- status?: ThenableStatus;
6
- value?: T;
5
+ status?: ThenableStatus | undefined;
6
+ value?: T | undefined;
7
7
  reason?: unknown;
8
8
  };
9
9
 
package/src/fileRoute.ts CHANGED
@@ -70,7 +70,7 @@ export class FileRoute<
70
70
  TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
71
71
  TFullPath extends RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
72
72
  > {
73
- silent?: boolean;
73
+ silent?: boolean | undefined;
74
74
 
75
75
  constructor(
76
76
  public path?: TFilePath,
@@ -111,10 +111,11 @@ function buildTagsFromMatches(
111
111
  }
112
112
 
113
113
  if (manifest.inlineStyle) {
114
+ const children = manifest.inlineStyle.children;
114
115
  manifestTags.push({
115
116
  tag: 'style',
116
117
  attrs: { ...manifest.inlineStyle.attrs, nonce },
117
- children: manifest.inlineStyle.children,
118
+ ...(children === undefined ? undefined : { children }),
118
119
  inlineCss: true,
119
120
  });
120
121
  }
@@ -123,25 +124,19 @@ function buildTagsFromMatches(
123
124
  const styles = matches
124
125
  .flatMap((match) => match.styles ?? [])
125
126
  .filter((style) => style !== undefined)
126
- .map(
127
- ({ children, ...attrs }) =>
128
- ({
129
- tag: 'style',
130
- attrs: { ...attrs, nonce },
131
- children: children,
132
- }) satisfies RouterManagedTag,
127
+ .map(({ children, ...attrs }): RouterManagedTag =>
128
+ children === undefined
129
+ ? { tag: 'style', attrs: { ...attrs, nonce } }
130
+ : { tag: 'style', attrs: { ...attrs, nonce }, children },
133
131
  );
134
132
 
135
133
  const headScripts = matches
136
134
  .flatMap((match) => match.headScripts ?? [])
137
135
  .filter((script) => script !== undefined)
138
- .map(
139
- ({ children, ...attrs }) =>
140
- ({
141
- tag: 'script',
142
- attrs: { ...attrs, nonce },
143
- children: children,
144
- }) satisfies RouterManagedTag,
136
+ .map(({ children, ...attrs }): RouterManagedTag =>
137
+ children === undefined
138
+ ? { tag: 'script', attrs: { ...attrs, nonce } }
139
+ : { tag: 'script', attrs: { ...attrs, nonce }, children },
145
140
  );
146
141
 
147
142
  const tags: Array<RouterManagedTag> = [];
package/src/history.ts CHANGED
@@ -1,10 +1,18 @@
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,
1
+ import {
2
+ createBrowserHistory as createTanStackBrowserHistory,
3
+ createHashHistory as createTanStackHashHistory,
9
4
  } from '@tanstack/history';
5
+ import { prepareInitialHistoryState } from './historyState';
6
+
7
+ export { createHistory, createMemoryHistory } from '@tanstack/history';
10
8
  export type * from '@tanstack/history';
9
+
10
+ export const createBrowserHistory: typeof createTanStackBrowserHistory = (options) => {
11
+ prepareInitialHistoryState(options?.window);
12
+ return createTanStackBrowserHistory(options);
13
+ };
14
+
15
+ export const createHashHistory: typeof createTanStackHashHistory = (options) => {
16
+ prepareInitialHistoryState(options?.window);
17
+ return createTanStackHashHistory(options);
18
+ };
@@ -0,0 +1,37 @@
1
+ import { parseHref } from '@tanstack/history';
2
+
3
+ type NavigationWindow = Window & {
4
+ navigation?: {
5
+ currentEntry?: { key: string; getState(): unknown } | null;
6
+ updateCurrentEntry(options: { state: unknown }): void;
7
+ };
8
+ };
9
+
10
+ export function prepareInitialHistoryState(
11
+ win: NavigationWindow | undefined = typeof document === 'undefined' ? undefined : window,
12
+ ): void {
13
+ if (!win) return;
14
+ const state = win.history.state;
15
+ if (state?.__TSR_key || state?.key) return;
16
+
17
+ const navigation = win.navigation;
18
+ const entry = navigation?.currentEntry;
19
+ const navigationState =
20
+ entry && typeof navigation?.updateCurrentEntry === 'function' ? entry.getState() : undefined;
21
+ if (state == null && navigationState === undefined) return;
22
+ const entryKey = entry?.key;
23
+
24
+ // Seed TanStack's metadata before its constructor replaces the entry. The
25
+ // History API and Navigation API store independent state for the same entry.
26
+ const metadata = parseHref('/', undefined).state;
27
+ win.history.replaceState(
28
+ { ...state, ...metadata, __TSR_index: state?.__TSR_index ?? metadata.__TSR_index },
29
+ '',
30
+ );
31
+ if (navigation && navigationState !== undefined) {
32
+ const currentEntry = navigation.currentEntry;
33
+ if (currentEntry && currentEntry.key === entryKey && currentEntry.getState() === undefined) {
34
+ navigation.updateCurrentEntry({ state: navigationState });
35
+ }
36
+ }
37
+ }
@@ -1,3 +1,5 @@
1
+ 'use strong';
2
+
1
3
  // The read hooks — ports of react-router's useMatch.tsx / useParams / useSearch /
2
4
  // useLoaderData / useLoaderDeps / useRouteContext / useNavigate / useCanGoBack /
3
5
  // Matches.tsx (useMatches / useParentMatches / useChildMatches). Everything match-
package/src/index.ts CHANGED
@@ -122,8 +122,8 @@ export type {
122
122
  } from './routeHookTypes';
123
123
  export { useAwaited } from './useAwaited';
124
124
  export type { AwaitOptions } from './useAwaited';
125
- export { useLinkProps, createLink, linkOptions } from './link';
126
- export type { LinkOptionsFn, LinkOptionsFnOptions } from './link';
125
+ export { useLinkProps, createLink, linkOptions } from './linkApi';
126
+ export type { LinkOptionsFn, LinkOptionsFnOptions } from './linkApi';
127
127
  export type {
128
128
  ActiveLinkOptionProps,
129
129
  ActiveLinkOptions,
@@ -166,8 +166,8 @@ export { Scripts } from './Scripts.tsrx';
166
166
  export { ScriptOnce } from './ScriptOnce.tsrx';
167
167
  export { Asset } from './Asset.tsrx';
168
168
  export type { AssetProps } from './Asset.tsrx';
169
- export { getAssetKeys } from './assetKeys.ts';
170
- export type { AssetKeys } from './assetKeys.ts';
169
+ export { getAssetKeys } from './assetKeys';
170
+ export type { AssetKeys } from './assetKeys';
171
171
  export { useTags } from './headContentUtils';
172
172
  export { Html } from './Html';
173
173
  export type { HtmlProps } from './Html';
@@ -1,13 +1,11 @@
1
1
  import { createElement } from 'octane';
2
2
  import type { ComponentBody } from 'octane';
3
3
  import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
4
- import { useRouter } from './context';
5
4
  import { Link } from './Link.tsrx';
6
- import { createUseLinkProps } from './linkBinding';
7
5
  import type { LinkComponent } from './linkTypes';
8
6
  import type { ValidateLinkOptions, ValidateLinkOptionsArray } from './typePrimitives';
9
7
 
10
- export const useLinkProps = createUseLinkProps(useRouter);
8
+ export { useLinkProps } from './Link.tsrx';
11
9
 
12
10
  // Wrap a design-system component so it navigates like <Link> — the component
13
11
  // receives the fully-built link props (href, handlers, data-status, …).
@@ -1,3 +1,5 @@
1
+ 'use strong';
2
+
1
3
  // Shared useLinkProps behavior — port of react-router's link.tsx
2
4
  // (client path; SSR link rendering arrives with the SSR entries). All of Link's
3
5
  // behavior lives here so both `<Link>` and custom `createLink` components share
@@ -8,7 +10,7 @@
8
10
  // IntersectionObserver, 'render' once on mount), the click handler (navigate
9
11
  // with replace/resetScroll/hashScrollIntoView/viewTransition/startTransition/
10
12
  // ignoreBlocker forwarded), and data-status/data-transitioning reflection.
11
- import { useRef, useState, useEffect, useCallback, flushSync } from 'octane';
13
+ import { __useCommittedState, useRef, useEffect, useCallback } from 'octane';
12
14
  import {
13
15
  deepEqual,
14
16
  exactPathTest,
@@ -216,21 +218,24 @@ export function createUseLinkProps(useRouter: typeof UseRouter) {
216
218
  // Class composes clsx-style (octane normalizeClass folds arrays + falsy).
217
219
  const resolvedClass = [
218
220
  klass ?? className,
219
- resolvedActiveProps.class ?? resolvedActiveProps.className,
220
- resolvedInactiveProps.class ?? resolvedInactiveProps.className,
221
+ resolvedActiveProps['class'] ?? resolvedActiveProps['className'],
222
+ resolvedInactiveProps['class'] ?? resolvedInactiveProps['className'],
221
223
  ].filter(Boolean);
222
224
  const resolvedStyle = mergeStyles(
223
225
  style,
224
- resolvedActiveProps.style,
225
- resolvedInactiveProps.style,
226
+ resolvedActiveProps['style'],
227
+ resolvedInactiveProps['style'],
226
228
  );
227
229
 
228
- const [isTransitioning, setIsTransitioning] = useState(false, subSlot(slot, 'lp:t'));
230
+ const [isTransitioning, setIsTransitioning, commitIsTransitioning] = __useCommittedState(
231
+ false,
232
+ subSlot(slot, 'lp:t'),
233
+ );
229
234
  const hasRenderFetched = useRef(false, subSlot(slot, 'lp:rf'));
230
235
  const elRef = useRef<Element | null>(null, subSlot(slot, 'lp:el'));
231
236
 
232
237
  const preload =
233
- options.reloadDocument || externalLink
238
+ options['reloadDocument'] || externalLink
234
239
  ? false
235
240
  : (userPreload ?? router.options.defaultPreload);
236
241
  const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
@@ -246,6 +251,7 @@ export function createUseLinkProps(useRouter: typeof UseRouter) {
246
251
  subSlot(slot, 'lp:dp'),
247
252
  );
248
253
 
254
+ // This manually slotted module retains explicit effect dependencies.
249
255
  // preload="viewport": preload when the anchor scrolls into view (100px margin).
250
256
  useEffect(
251
257
  () => {
@@ -292,9 +298,7 @@ export function createUseLinkProps(useRouter: typeof UseRouter) {
292
298
  ) {
293
299
  e.preventDefault();
294
300
 
295
- flushSync(() => {
296
- setIsTransitioning(true);
297
- });
301
+ commitIsTransitioning(true);
298
302
  const unsub = router.subscribe('onResolved', () => {
299
303
  unsub();
300
304
  setIsTransitioning(false);
@@ -11,9 +11,9 @@
11
11
  // no forwardRef); `disabled` renders without an href + role="link" (upstream
12
12
  // drops the anchor's disabled attribute; the aria props carry the state).
13
13
  import { isChildrenBlock } from 'octane';
14
- import type { useLinkProps as UseLinkProps } from './link.ts';
14
+ import type { useLinkProps as UseLinkProps } from './linkApi';
15
15
  import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
16
- import type { LinkComponentProps, OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes.ts';
16
+ import type { LinkComponentProps, OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes';
17
17
 
18
18
  // Keep the generic implementation signature visible to `.tsrx` consumers so
19
19
  // route-aware `to`/`params`/`search` inference and native anchor event types are
@@ -51,7 +51,7 @@ export function createLinkComponent(useLinkProps: typeof UseLinkProps) {
51
51
  : children;
52
52
 
53
53
  @if (AsChild) {
54
- <AsChild {...linkProps}>{resolvedChildren}</AsChild>
54
+ <AsChild {...linkProps} children={resolvedChildren} />
55
55
  } @else {
56
56
  <a {...(aProps as Record<string, any>)}>{resolvedChildren}</a>
57
57
  }