@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.
- package/README.md +23 -7
- package/package.json +4 -3
- package/src/Asset.tsrx +1 -1
- package/src/Await.tsrx +1 -1
- package/src/CatchBoundary.tsrx +11 -41
- package/src/ClientOnly.tsrx +1 -1
- package/src/ErrorComponent.tsrx +38 -0
- package/src/HeadContent.tsrx +4 -2
- package/src/Link.tsrx +3 -1
- package/src/Link.tsrx.d.ts +3 -0
- package/src/Match.tsrx +3 -3
- package/src/MatchRoute.tsrx +5 -5
- package/src/Matches.tsrx +6 -19
- package/src/Navigate.tsrx +1 -1
- package/src/Outlet.tsrx +2 -2
- package/src/RouteNotFound.tsrx +1 -1
- package/src/RouterProvider.tsrx +1 -1
- package/src/ScriptOnce.tsrx +14 -15
- package/src/Scripts.tsrx +1 -1
- package/src/ScrollRestoration.tsrx +1 -1
- package/src/Transitioner.tsrx +80 -62
- package/src/assetComponent.tsrx +1 -1
- package/src/compiled/Document.tsrx +2 -0
- package/src/compiled/Link.tsrx +1 -3
- package/src/compiled/Matches.tsrx +128 -120
- package/src/compiled/RouterProvider.tsrx +27 -0
- package/src/compiled.ts +6 -3
- package/src/core.ts +8 -0
- package/src/externalHydration.ts +2 -2
- package/src/fileRoute.ts +1 -1
- package/src/headContentBinding.ts +10 -15
- package/src/history.ts +16 -8
- package/src/historyState.ts +37 -0
- package/src/hookBindings.ts +2 -0
- package/src/index.ts +4 -4
- package/src/{link.ts → linkApi.ts} +1 -3
- package/src/linkBinding.ts +14 -10
- package/src/linkComponent.tsrx +3 -3
- package/src/not-found.tsrx +4 -4
- package/src/route.ts +2 -1
- package/src/router.ts +5 -0
- package/src/routerStateBinding.ts +2 -0
- package/src/scriptComponent.tsrx +22 -15
- package/src/scroll-restoration.tsrx +1 -1
- package/src/ssr/RouterClient.tsrx +1 -1
- package/src/ssr/renderRouterToStream.ts +13 -4
- package/src/useBlocker.tsrx +1 -1
- package/src/useStore.ts +2 -0
package/README.md
CHANGED
|
@@ -62,6 +62,16 @@ tree renders **pull-based**: `RouterProvider` renders the first match, and each
|
|
|
62
62
|
component's `<Outlet/>` looks up the next match via `matchContext` — so navigation
|
|
63
63
|
re-renders only the matches that changed.
|
|
64
64
|
|
|
65
|
+
## Headless routing
|
|
66
|
+
|
|
67
|
+
Use `@octanejs/tanstack-router/core` when an application owns its render tree and
|
|
68
|
+
only needs matching, loading, and navigation. It exposes Octane's router adapter,
|
|
69
|
+
the framework-neutral `@tanstack/router-core` APIs, and history constructors
|
|
70
|
+
without importing route components or hooks.
|
|
71
|
+
|
|
72
|
+
Browser and hash histories created by this package preserve existing application
|
|
73
|
+
fields in History API state objects and Navigation API entry state during initialization.
|
|
74
|
+
|
|
65
75
|
## Same-route search navigation
|
|
66
76
|
|
|
67
77
|
Same-route search navigations now render store updates urgently. If the route
|
|
@@ -144,6 +154,12 @@ existing `CompiledRouterProvider`, `CompiledOutlet`, and
|
|
|
144
154
|
`lazyCompiledRouteComponent` names remain aliases. TanStack owns matching,
|
|
145
155
|
loading, navigation, history and render acknowledgements.
|
|
146
156
|
|
|
157
|
+
`PreconfiguredRouterProvider` accepts only `{ router }` and owns the provider,
|
|
158
|
+
match projection, and failure presentation in Strong mode. The compiled error
|
|
159
|
+
boundary owns synchronous route error reporting and not-found propagation.
|
|
160
|
+
Configure the router before rendering it. Use `RouterProvider` when provider
|
|
161
|
+
option props need to reconfigure the router synchronously.
|
|
162
|
+
|
|
147
163
|
```tsx
|
|
148
164
|
import {
|
|
149
165
|
createRootRoute, createRoute, createRouter, createBrowserHistory,
|
|
@@ -191,13 +207,13 @@ Compiled selection removes descriptor creation from route selection, bound
|
|
|
191
207
|
links and document shells, but does not make the entire binding Strong or
|
|
192
208
|
remove the generic renderer. Link retains synchronous `flushSync` so its
|
|
193
209
|
transition cue is already visible in `onBeforeNavigate` callbacks. Both entries
|
|
194
|
-
share a
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
share a Strong keyed script template. Newly mounted route scripts execute once;
|
|
211
|
+
hydration adopts existing scripts without replaying them, and self-removing
|
|
212
|
+
transport scripts leave no item markers behind. Managed keys, nonces and route
|
|
213
|
+
ownership are retained. Scroll-restoration scripts use a server-owned static
|
|
214
|
+
hydration range, which is removed after hydration and omitted on client-only
|
|
215
|
+
mounts. Error propagation and lifecycle dependencies retain their existing
|
|
216
|
+
contracts.
|
|
201
217
|
|
|
202
218
|
`Wrap` and `InnerWrap` remain unsupported and produce an explicit diagnostic.
|
|
203
219
|
For returned JSX, host strings, renderable wrappers or mixed route trees, retain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"exports": {
|
|
38
38
|
".": "./src/index.ts",
|
|
39
|
+
"./core": "./src/core.ts",
|
|
39
40
|
"./history": "./src/history.ts",
|
|
40
41
|
"./compiled": "./src/compiled.ts",
|
|
41
42
|
"./ssr/server": {
|
|
@@ -59,13 +60,13 @@
|
|
|
59
60
|
"isbot": "^5.1.22"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
|
-
"octane": "^0.
|
|
63
|
+
"octane": "^0.14.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@tanstack/react-router": "1.170.41",
|
|
66
67
|
"@tsrx/react": "^0.4.5",
|
|
67
68
|
"esbuild": "^0.28.1",
|
|
68
|
-
"octane": "0.
|
|
69
|
+
"octane": "0.14.0",
|
|
69
70
|
"react": "19.2.7",
|
|
70
71
|
"react-dom": "19.2.7",
|
|
71
72
|
"vitest": "^4.1.10"
|
package/src/Asset.tsrx
CHANGED
package/src/Await.tsrx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// render-prop, so the consumer's `<Await>{(d) => …}</Await>` stays the public API).
|
|
6
6
|
import { use } from 'octane';
|
|
7
7
|
import type { OctaneNode } from 'octane';
|
|
8
|
-
import { toExternalHydrationThenable } from './externalHydration
|
|
8
|
+
import { toExternalHydrationThenable } from './externalHydration';
|
|
9
9
|
|
|
10
10
|
function AwaitInner(props: { promise: Promise<any>; render: (data: any) => OctaneNode }) {
|
|
11
11
|
const data = use(toExternalHydrationThenable(props.promise));
|
package/src/CatchBoundary.tsrx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// react-router's CatchBoundary
|
|
1
|
+
// react-router's CatchBoundary, rebuilt on octane's
|
|
2
2
|
// `@try`/`@catch` (octane has no class components, so the upstream
|
|
3
3
|
// CatchBoundaryImpl class becomes a catch branch). Contract preserved from
|
|
4
4
|
// upstream CatchBoundary.tsx:
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
// getDerivedStateFromProps; here a layout effect calls the catch reset — one
|
|
14
14
|
// commit later, same observable outcome). Match passes the router's `loadedAt`
|
|
15
15
|
// so navigation clears route error UI.
|
|
16
|
-
import {
|
|
16
|
+
import { useRef, useLayoutEffect } from 'octane';
|
|
17
17
|
import type { OctaneNode } from 'octane';
|
|
18
|
-
import type { ErrorInfo, ErrorRouteComponent } from './route
|
|
18
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route';
|
|
19
|
+
import { ErrorComponent } from './ErrorComponent.tsrx';
|
|
20
|
+
|
|
21
|
+
export { ErrorComponent };
|
|
19
22
|
|
|
20
23
|
export function CatchBoundary(props: {
|
|
21
24
|
getResetKey: () => unknown;
|
|
22
25
|
children: OctaneNode;
|
|
23
|
-
errorComponent?: ErrorRouteComponent;
|
|
24
|
-
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
26
|
+
errorComponent?: ErrorRouteComponent | undefined;
|
|
27
|
+
onCatch?: ((error: Error, errorInfo: ErrorInfo) => void) | undefined;
|
|
25
28
|
}) @{
|
|
26
29
|
@try {
|
|
27
30
|
<>{props.children}</>
|
|
@@ -39,9 +42,9 @@ export function CatchBoundary(props: {
|
|
|
39
42
|
function CatchErrorView(props: {
|
|
40
43
|
error: any;
|
|
41
44
|
reset: () => void;
|
|
42
|
-
getResetKey
|
|
43
|
-
errorComponent?: ErrorRouteComponent;
|
|
44
|
-
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
45
|
+
getResetKey: () => unknown;
|
|
46
|
+
errorComponent?: ErrorRouteComponent | undefined;
|
|
47
|
+
onCatch?: ((error: Error, errorInfo: ErrorInfo) => void) | undefined;
|
|
45
48
|
}) @{
|
|
46
49
|
// componentDidCatch parity — once per distinct caught error, during render so a
|
|
47
50
|
// rethrow (the notFound-forwarding path) escapes to the parent boundary.
|
|
@@ -66,36 +69,3 @@ function CatchErrorView(props: {
|
|
|
66
69
|
|
|
67
70
|
<ErrorComp error={props.error} reset={props.reset} info={{ componentStack: '' }} />
|
|
68
71
|
}
|
|
69
|
-
|
|
70
|
-
// The default error UI, markup-identical to react-router's ErrorComponent (string
|
|
71
|
-
// styles serialize to the same inline css React produces, so the differential rig
|
|
72
|
-
// can byte-compare it).
|
|
73
|
-
export function ErrorComponent(props: {
|
|
74
|
-
error: any;
|
|
75
|
-
reset?: () => void;
|
|
76
|
-
info?: { componentStack?: string };
|
|
77
|
-
}) @{
|
|
78
|
-
const [show, setShow] = useState(process.env.NODE_ENV !== 'production');
|
|
79
|
-
|
|
80
|
-
<div style="padding:.5rem;max-width:100%">
|
|
81
|
-
<div style="display:flex;align-items:center;gap:.5rem">
|
|
82
|
-
<strong style="font-size:1rem">Something went wrong!</strong>
|
|
83
|
-
<button
|
|
84
|
-
style="appearance:none;font-size:.6em;border:1px solid currentColor;padding:.1rem .2rem;font-weight:bold;border-radius:.25rem"
|
|
85
|
-
onClick={() => setShow((d) => !d)}
|
|
86
|
-
>
|
|
87
|
-
{show ? 'Hide Error' : 'Show Error'}
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
<div style="height:.25rem"></div>
|
|
91
|
-
@if (show) {
|
|
92
|
-
<div>
|
|
93
|
-
<pre style="font-size:.7em;border:1px solid red;border-radius:.25rem;padding:.3rem;color:red;overflow:auto">
|
|
94
|
-
@if (props.error?.message) {
|
|
95
|
-
<code>{props.error.message as string}</code>
|
|
96
|
-
}
|
|
97
|
-
</pre>
|
|
98
|
-
</div>
|
|
99
|
-
}
|
|
100
|
-
</div>
|
|
101
|
-
}
|
package/src/ClientOnly.tsrx
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strong';
|
|
2
|
+
|
|
3
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
4
|
+
|
|
5
|
+
import { useState } from 'octane';
|
|
6
|
+
|
|
7
|
+
// The default error UI, markup-identical to react-router's ErrorComponent (string
|
|
8
|
+
// styles serialize to the same inline css React produces, so the differential rig
|
|
9
|
+
// can byte-compare it).
|
|
10
|
+
export function ErrorComponent(props: {
|
|
11
|
+
error: any;
|
|
12
|
+
reset?: () => void;
|
|
13
|
+
info?: { componentStack?: string };
|
|
14
|
+
}) @{
|
|
15
|
+
const [show, setShow] = useState(process.env.NODE_ENV !== 'production');
|
|
16
|
+
|
|
17
|
+
<div style="padding:.5rem;max-width:100%">
|
|
18
|
+
<div style="display:flex;align-items:center;gap:.5rem">
|
|
19
|
+
<strong style="font-size:1rem">Something went wrong!</strong>
|
|
20
|
+
<button
|
|
21
|
+
style="appearance:none;font-size:.6em;border:1px solid currentColor;padding:.1rem .2rem;font-weight:bold;border-radius:.25rem"
|
|
22
|
+
onClick={() => setShow((d) => !d)}
|
|
23
|
+
>
|
|
24
|
+
{show ? 'Hide Error' : 'Show Error'}
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="height:.25rem"></div>
|
|
28
|
+
@if (show) {
|
|
29
|
+
<div>
|
|
30
|
+
<pre style="font-size:.7em;border:1px solid red;border-radius:.25rem;padding:.3rem;color:red;overflow:auto">
|
|
31
|
+
@if (props.error?.message) {
|
|
32
|
+
<code>{props.error.message as string}</code>
|
|
33
|
+
}
|
|
34
|
+
</pre>
|
|
35
|
+
</div>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
}
|
package/src/HeadContent.tsrx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
2
|
+
|
|
1
3
|
import { createElement, useEffect, useId } from 'octane';
|
|
2
4
|
import { DEV_STYLES_ATTR } from '@tanstack/router-core';
|
|
3
5
|
import type { AssetCrossOriginConfig } from '@tanstack/router-core';
|
|
4
6
|
import { Asset } from './Asset.tsrx';
|
|
5
7
|
import type { AssetProps } from './Asset.tsrx';
|
|
6
|
-
import { getAssetKeys } from './assetKeys
|
|
7
|
-
import { useTags } from './headContentUtils
|
|
8
|
+
import { getAssetKeys } from './assetKeys';
|
|
9
|
+
import { useTags } from './headContentUtils';
|
|
8
10
|
import { useHydrated } from './ClientOnly.tsrx';
|
|
9
11
|
|
|
10
12
|
export interface HeadContentProps {
|
package/src/Link.tsrx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRouter } from './context';
|
|
2
|
+
import { createUseLinkProps } from './linkBinding';
|
|
2
3
|
import { createLinkComponent } from './linkComponent.tsrx';
|
|
3
4
|
|
|
5
|
+
export const useLinkProps = createUseLinkProps(useRouter);
|
|
4
6
|
export const Link = createLinkComponent(useLinkProps);
|
package/src/Link.tsrx.d.ts
CHANGED
package/src/Match.tsrx
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
import { createElement, Suspense } from 'octane';
|
|
21
21
|
import { isNotFound, rootRouteId } from '@tanstack/router-core';
|
|
22
22
|
import type { NotFoundError, RootRouteOptions } from '@tanstack/router-core';
|
|
23
|
-
import type { ErrorInfo, ErrorRouteComponent } from './route
|
|
24
|
-
import { useStore } from './useStore
|
|
25
|
-
import { useRouter, matchContext as MatchContext } from './context
|
|
23
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route';
|
|
24
|
+
import { useStore } from './useStore';
|
|
25
|
+
import { useRouter, matchContext as MatchContext } from './context';
|
|
26
26
|
import { Outlet } from './Outlet.tsrx';
|
|
27
27
|
import { RouteNotFound } from './RouteNotFound.tsrx';
|
|
28
28
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
package/src/MatchRoute.tsrx
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// false | matched params. The component renders its children when matched — a
|
|
6
6
|
// render-prop child ALWAYS renders, receiving the params (false when unmatched).
|
|
7
7
|
import { useCallback, isChildrenBlock } from 'octane';
|
|
8
|
-
import { useRouter } from './context
|
|
9
|
-
import { useStore } from './useStore
|
|
8
|
+
import { useRouter } from './context';
|
|
9
|
+
import { useStore } from './useStore';
|
|
10
10
|
|
|
11
11
|
export function useMatchRoute() {
|
|
12
12
|
const router = useRouter();
|
|
@@ -29,10 +29,10 @@ export function MatchRoute(props: Record<string, any>) @{
|
|
|
29
29
|
const matchRoute = useMatchRoute();
|
|
30
30
|
const params = matchRoute(props);
|
|
31
31
|
const out =
|
|
32
|
-
typeof props
|
|
33
|
-
? props
|
|
32
|
+
typeof props['children'] === 'function' && !isChildrenBlock(props['children'])
|
|
33
|
+
? props['children'](params)
|
|
34
34
|
: params
|
|
35
|
-
? props
|
|
35
|
+
? props['children']
|
|
36
36
|
: null;
|
|
37
37
|
<>{out}</>
|
|
38
38
|
}
|
package/src/Matches.tsrx
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
// ErrorComponent for errors no route boundary caught, reset by `loadedAt`.
|
|
13
13
|
import { useLayoutEffect, createElement, Suspense } from 'octane';
|
|
14
14
|
import { setupScrollRestoration, rootRouteId } from '@tanstack/router-core';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { useTransitioner, settleOwner } from './Transitioner.tsrx';
|
|
15
|
+
import { useRouter, matchContext as MatchContext } from './context';
|
|
16
|
+
import { useTransitioner, usePresentedMatches } from './Transitioner.tsrx';
|
|
18
17
|
import { Match } from './Match.tsrx';
|
|
19
18
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
20
19
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
@@ -32,34 +31,22 @@ export function Matches() @{
|
|
|
32
31
|
useLayoutEffect(() => {
|
|
33
32
|
if (router.options.scrollRestoration) setupScrollRestoration(router);
|
|
34
33
|
}, [router]);
|
|
35
|
-
|
|
34
|
+
useTransitioner(router);
|
|
36
35
|
|
|
37
36
|
<InnerWrap>
|
|
38
37
|
<Suspense fallback={pendingElement}>
|
|
39
|
-
<MatchesInner
|
|
38
|
+
<MatchesInner />
|
|
40
39
|
</Suspense>
|
|
41
40
|
</InnerWrap>
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
function MatchesInner(
|
|
43
|
+
function MatchesInner() @{
|
|
45
44
|
const router = useRouter();
|
|
46
|
-
|
|
47
|
-
// is `matches[0]` and the global error boundary resets on its identity. While a
|
|
48
|
-
// navigation transition is pending, render the matches router-core expects
|
|
49
|
-
// (`acknowledgement[0]`) so the committed tree equals what it awaits, then
|
|
50
|
-
// settle that render acknowledgement from a layout effect once it lands.
|
|
51
|
-
const acknowledgement: Array<any> = ((router as any)._rendered ??= []);
|
|
52
|
-
const matches = useStore(router.stores.matches, (m: any) =>
|
|
53
|
-
acknowledgement[0] === props.expectedMatches ? (props.expectedMatches ?? m) : m,
|
|
54
|
-
) as any;
|
|
45
|
+
const matches = usePresentedMatches(router);
|
|
55
46
|
const rootMatch = matches[0];
|
|
56
47
|
const routeId = rootMatch?.routeId as string | undefined;
|
|
57
48
|
const CatchWrap = router.options.disableGlobalCatchBoundary ? SafeFragment : CatchBoundary;
|
|
58
49
|
|
|
59
|
-
useLayoutEffect(() => {
|
|
60
|
-
if (acknowledgement[0] === matches) settleOwner(acknowledgement, true);
|
|
61
|
-
}, [acknowledgement, matches]);
|
|
62
|
-
|
|
63
50
|
<MatchContext value={routeId}>
|
|
64
51
|
<CatchWrap getResetKey={() => rootMatch} errorComponent={ErrorComponent}>
|
|
65
52
|
@if (routeId) {
|
package/src/Navigate.tsrx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Imperative navigation as a component: navigates once on mount, renders nothing.
|
|
2
2
|
import { useLayoutEffect, useRef } from 'octane';
|
|
3
3
|
import type { NavigateOptions } from '@tanstack/router-core';
|
|
4
|
-
import { useNavigate } from './hooks
|
|
4
|
+
import { useNavigate } from './hooks';
|
|
5
5
|
|
|
6
6
|
export function Navigate(props: NavigateOptions) @{
|
|
7
7
|
const navigate = useNavigate();
|
package/src/Outlet.tsrx
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// match — so the 404 lands inside the layout chrome.
|
|
12
12
|
import { useContext, createElement, Suspense } from 'octane';
|
|
13
13
|
import { rootRouteId } from '@tanstack/router-core';
|
|
14
|
-
import { useStore } from './useStore
|
|
15
|
-
import { useRouter, matchContext } from './context
|
|
14
|
+
import { useStore } from './useStore';
|
|
15
|
+
import { useRouter, matchContext } from './context';
|
|
16
16
|
import { Match } from './Match.tsrx';
|
|
17
17
|
import { RouteNotFound } from './RouteNotFound.tsrx';
|
|
18
18
|
import { SafeFragment } from './SafeFragment.tsrx';
|
package/src/RouteNotFound.tsrx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// when the match resolved with `status === 'notFound'` (a loader threw
|
|
9
9
|
// `notFound()`). Upstream spreads the NotFoundError onto the component, so a
|
|
10
10
|
// `notFound({ data })` payload arrives as the `data` prop.
|
|
11
|
-
import { useRouter } from './context
|
|
11
|
+
import { useRouter } from './context';
|
|
12
12
|
|
|
13
13
|
export function RouteNotFound(props: { routeId: string; error?: any }) @{
|
|
14
14
|
const router = useRouter();
|
package/src/RouterProvider.tsrx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { hasKeys } from '@tanstack/router-core';
|
|
8
8
|
import type { AnyRouter } from '@tanstack/router-core';
|
|
9
9
|
import type { OctaneNode } from 'octane';
|
|
10
|
-
import { routerContext as RouterContext } from './context
|
|
10
|
+
import { routerContext as RouterContext } from './context';
|
|
11
11
|
import { Matches } from './Matches.tsrx';
|
|
12
12
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
13
13
|
|
package/src/ScriptOnce.tsrx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// may not have it) and the post-hydration render drops it. A client-only mount
|
|
7
|
-
// renders nothing, as upstream does.
|
|
1
|
+
'use strong';
|
|
2
|
+
|
|
3
|
+
// The browser build has no scroll-script source. Preserve the server-owned
|
|
4
|
+
// range without reconciling its content, then remove it after hydration commits.
|
|
5
|
+
// A client-only mount has no server range and renders nothing.
|
|
8
6
|
import { isServer } from '@tanstack/router-core/isServer';
|
|
9
|
-
import {
|
|
7
|
+
import { Hydrate, trustHTML } from 'octane';
|
|
8
|
+
import { never } from 'octane/hydration';
|
|
9
|
+
import { useRouter } from './context';
|
|
10
10
|
import { useHydrated } from './ClientOnly.tsrx';
|
|
11
11
|
|
|
12
12
|
export function ScriptOnce(props: { children: string }) @{
|
|
@@ -14,13 +14,12 @@ export function ScriptOnce(props: { children: string }) @{
|
|
|
14
14
|
const hydrated = useHydrated();
|
|
15
15
|
|
|
16
16
|
@if ((isServer ?? router.isServer) || !hydrated) {
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/>
|
|
17
|
+
<Hydrate split={false} when={never()}>
|
|
18
|
+
<script
|
|
19
|
+
nonce={router.options.ssr?.nonce}
|
|
20
|
+
dangerouslySetInnerHTML={trustHTML(props.children)}
|
|
21
|
+
/>
|
|
22
|
+
</Hydrate>
|
|
24
23
|
} @else {
|
|
25
24
|
<></>
|
|
26
25
|
}
|
package/src/Scripts.tsrx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createScripts } from './scriptComponent.tsrx';
|
|
2
|
-
import { useScripts } from './scriptContentUtils
|
|
2
|
+
import { useScripts } from './scriptContentUtils';
|
|
3
3
|
|
|
4
4
|
// router-core 1.171.34 owns the hydration-script lifecycle: the server appends
|
|
5
5
|
// the initial hydration tags (and the streaming boundary) through
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Matches); use one or the other.
|
|
6
6
|
import { useLayoutEffect } from 'octane';
|
|
7
7
|
import { setupScrollRestoration } from '@tanstack/router-core';
|
|
8
|
-
import { useRouter } from './context
|
|
8
|
+
import { useRouter } from './context';
|
|
9
9
|
|
|
10
10
|
export function ScrollRestoration() @{
|
|
11
11
|
const router = useRouter();
|
package/src/Transitioner.tsrx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strong';
|
|
2
|
+
|
|
1
3
|
// The navigation engine — port of react-router's Transitioner. It runs as a hook
|
|
2
4
|
// because a rendered sibling would place an SSR marker before a root route's
|
|
3
5
|
// document-level <html> node and outside the #__app hydration range.
|
|
@@ -17,81 +19,97 @@
|
|
|
17
19
|
// and kick off the initial load — or, when hydrating an already-resolved SSR
|
|
18
20
|
// location, acknowledge the first render so `onRendered` consumers (scroll
|
|
19
21
|
// restoration, devtools) fire.
|
|
20
|
-
import {
|
|
22
|
+
import { useInsertionEffect, useLayoutEffect, startTransition } from 'octane';
|
|
21
23
|
import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
|
|
22
|
-
import type { AnyRouter } from '@tanstack/router-core';
|
|
24
|
+
import type { AnyRouteMatch, AnyRouter } from '@tanstack/router-core';
|
|
25
|
+
import { useStore } from './useStore';
|
|
23
26
|
|
|
24
27
|
// Resolve the pending render acknowledgement with whether the expected matches
|
|
25
28
|
// rendered, then clear the slot. `owner` is `router._rendered`: `[expected, resolve]`.
|
|
26
|
-
|
|
27
|
-
const settle = owner[1]
|
|
29
|
+
function settleOwner(owner: NonNullable<AnyRouter['_rendered']>, rendered: boolean) {
|
|
30
|
+
const settle = owner[1];
|
|
28
31
|
owner.length = 0;
|
|
29
32
|
settle?.(rendered);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const acknowledgement: Array<any> = (router._rendered ??= []);
|
|
37
|
-
|
|
38
|
-
router.startTransition = (fn: () => void, expected: unknown) =>
|
|
35
|
+
function installTransitioner(router: AnyRouter) {
|
|
36
|
+
const acknowledgement = (router._rendered ??= []);
|
|
37
|
+
const previous = router.startTransition;
|
|
38
|
+
const owned: AnyRouter['startTransition'] = (fn, expected) =>
|
|
39
39
|
new Promise<boolean>((resolve) => {
|
|
40
|
-
// A newer transition supersedes an unrendered older one.
|
|
41
40
|
settleOwner(acknowledgement, false);
|
|
42
|
-
acknowledgement
|
|
43
|
-
|
|
44
|
-
// snapshot and acknowledges it only after that exact tree commits.
|
|
45
|
-
setExpectedMatches(expected);
|
|
41
|
+
acknowledgement[0] = expected;
|
|
42
|
+
acknowledgement[1] = resolve;
|
|
46
43
|
startTransition(fn);
|
|
47
44
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// mounted router and re-runs only when the router (or its history) is swapped
|
|
54
|
-
// on a live provider — exactly when the new router needs its initial load.
|
|
55
|
-
// Upstream guards this block with a dev-only `mounted` ref purely to absorb
|
|
56
|
-
// React's StrictMode double-invoke; keeping that ref here would instead skip
|
|
57
|
-
// the swapped-in router's load.
|
|
58
|
-
router.updateLatestLocation?.();
|
|
59
|
-
const location = router.latestLocation;
|
|
60
|
-
const nextLocation = router.buildLocation({
|
|
61
|
-
to: location.pathname,
|
|
62
|
-
search: true,
|
|
63
|
-
params: true,
|
|
64
|
-
hash: true,
|
|
65
|
-
state: true,
|
|
66
|
-
_includeValidateSearch: true,
|
|
67
|
-
} as any);
|
|
68
|
-
if (trimPathRight(location.publicHref) !== trimPathRight(nextLocation.publicHref)) {
|
|
69
|
-
router.commitLocation({ ...nextLocation, replace: true, ignoreBlocker: true });
|
|
70
|
-
return unsub;
|
|
45
|
+
router.startTransition = owned;
|
|
46
|
+
return () => {
|
|
47
|
+
if (router.startTransition === owned) {
|
|
48
|
+
router.startTransition = previous;
|
|
49
|
+
settleOwner(acknowledgement, false);
|
|
71
50
|
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else if (!router._tx) {
|
|
90
|
-
router.load({ sync: true }).catch((err: unknown) => {
|
|
91
|
-
console.error(err);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
54
|
+
function subscribeToHistory(router: AnyRouter, history: AnyRouter['history']) {
|
|
55
|
+
const unsub = history.subscribe(() => router.load());
|
|
56
|
+
|
|
57
|
+
// Re-run for a replacement router or history so the new instance loads.
|
|
58
|
+
router.updateLatestLocation?.();
|
|
59
|
+
const location = router.latestLocation;
|
|
60
|
+
const nextLocation = router.buildLocation({
|
|
61
|
+
to: location.pathname,
|
|
62
|
+
search: true,
|
|
63
|
+
params: true,
|
|
64
|
+
hash: true,
|
|
65
|
+
state: true,
|
|
66
|
+
_includeValidateSearch: true,
|
|
67
|
+
} as any);
|
|
68
|
+
if (trimPathRight(location.publicHref) !== trimPathRight(nextLocation.publicHref)) {
|
|
69
|
+
router.commitLocation({ ...nextLocation, replace: true, ignoreBlocker: true });
|
|
94
70
|
return unsub;
|
|
95
|
-
}
|
|
96
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const resolvedLocation = router.stores.resolvedLocation.get();
|
|
74
|
+
if (
|
|
75
|
+
resolvedLocation?.href === location.href &&
|
|
76
|
+
resolvedLocation.state.__TSR_key === location.state.__TSR_key
|
|
77
|
+
) {
|
|
78
|
+
// Hydration acknowledges the already-committed tree, which does not trigger
|
|
79
|
+
// another render for the navigation acknowledgement effect to observe.
|
|
80
|
+
router.emit({
|
|
81
|
+
type: 'onRendered',
|
|
82
|
+
...getLocationChangeInfo(resolvedLocation, resolvedLocation),
|
|
83
|
+
fromLocation: resolvedLocation,
|
|
84
|
+
});
|
|
85
|
+
} else if (!router._tx) {
|
|
86
|
+
router.load({ sync: true }).catch((err: unknown) => {
|
|
87
|
+
console.error(err);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return unsub;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function useTransitioner(router: AnyRouter) {
|
|
94
|
+
const history = router.history;
|
|
95
|
+
// Install before descendant layout effects can navigate, without mutating the
|
|
96
|
+
// router during render or placing a sibling before a document-level route.
|
|
97
|
+
useInsertionEffect(() => installTransitioner(router));
|
|
98
|
+
useLayoutEffect(() => subscribeToHistory(router, history));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function acknowledgeRenderedMatches(router: AnyRouter, matches: Array<AnyRouteMatch>) {
|
|
102
|
+
const owner = router._rendered;
|
|
103
|
+
if (owner && owner[0] === matches) settleOwner(owner, true);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The store may reconstruct its match array, so a navigation must present the
|
|
107
|
+
// exact snapshot offered by router-core until that tree commits. The live owner
|
|
108
|
+
// is observed only by the store snapshot and by the commit acknowledgement.
|
|
109
|
+
// Mirroring it in component state would entangle that update with an async action
|
|
110
|
+
// awaiting navigation, leaving both the action and its render acknowledgement pending.
|
|
111
|
+
export function usePresentedMatches(router: AnyRouter) {
|
|
112
|
+
const matches = useStore(router.stores.matches, (current) => router._rendered?.[0] ?? current);
|
|
113
|
+
useLayoutEffect(() => acknowledgeRenderedMatches(router, matches));
|
|
114
|
+
return matches;
|
|
97
115
|
}
|
package/src/assetComponent.tsrx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useLayoutEffect, useMemo, trustHTML } from 'octane';
|
|
4
4
|
import { isServer } from '@tanstack/router-core/isServer';
|
|
5
5
|
import type { RouterManagedTag } from '@tanstack/router-core';
|
|
6
|
-
import type { useRouter as UseRouter } from './context
|
|
6
|
+
import type { useRouter as UseRouter } from './context';
|
|
7
7
|
import { useHydrated } from './ClientOnly.tsrx';
|
|
8
8
|
|
|
9
9
|
export type AssetProps = RouterManagedTag & {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strong';
|
|
2
2
|
|
|
3
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
4
|
+
|
|
3
5
|
import { useEffect, useId, HYDRATION_RANGE_BOUNDARY, createPortal, useState } from 'octane';
|
|
4
6
|
import { DEV_STYLES_ATTR } from '@tanstack/router-core';
|
|
5
7
|
import type { AssetCrossOriginConfig } from '@tanstack/router-core';
|