@octanejs/tanstack-router 0.1.10 → 0.1.12
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 +24 -11
- package/package.json +26 -11
- package/src/Asset.tsrx +122 -0
- package/src/Asset.tsrx.d.ts +9 -0
- package/src/Await.tsrx +2 -1
- package/src/Await.tsrx.d.ts +8 -6
- package/src/Body.ts +31 -0
- package/src/CatchBoundary.tsrx +1 -3
- package/src/ClientOnly.tsrx +4 -2
- package/src/Head.ts +22 -0
- package/src/HeadContent.tsrx +41 -0
- package/src/HeadContent.tsrx.d.ts +8 -0
- package/src/Html.ts +19 -0
- package/src/Link.tsrx +25 -7
- package/src/Link.tsrx.d.ts +3 -4
- package/src/Match.tsrx +40 -13
- package/src/Matches.tsrx +7 -7
- package/src/Outlet.tsrx +3 -3
- package/src/RouteNotFound.tsrx +1 -1
- package/src/RouterProvider.tsrx +2 -1
- package/src/ScriptOnce.tsrx +23 -0
- package/src/ScriptOnce.tsrx.d.ts +3 -0
- package/src/Scripts.tsrx +42 -0
- package/src/Scripts.tsrx.d.ts +3 -0
- package/src/Transitioner.tsrx +15 -13
- package/src/assetKeys.ts +11 -0
- package/src/context.ts +6 -3
- package/src/externalHydration.ts +77 -0
- package/src/fileRoute.ts +277 -0
- package/src/frameworkTypes.ts +42 -0
- package/src/generator-plugin.d.ts +20 -0
- package/src/generator-plugin.js +108 -0
- package/src/headContentUtils.ts +172 -0
- package/src/hooks.ts +151 -0
- package/src/index.ts +84 -4
- package/src/lazyRouteComponent.ts +2 -1
- package/src/link.ts +25 -4
- package/src/linkTypes.ts +95 -0
- package/src/not-found.tsrx +2 -2
- package/src/octane-compiler.d.ts +12 -0
- package/src/route.ts +438 -42
- package/src/routeHookTypes.ts +228 -0
- package/src/router.ts +31 -6
- package/src/scriptContentUtils.ts +64 -0
- package/src/scroll-restoration.tsrx +16 -0
- package/src/scroll-restoration.tsrx.d.ts +3 -0
- package/src/ssr/RouterClient.tsrx +36 -0
- package/src/ssr/RouterClient.tsrx.d.ts +4 -0
- package/src/ssr/RouterServer.tsrx +6 -0
- package/src/ssr/RouterServer.tsrx.d.ts +4 -0
- package/src/ssr/client.ts +4 -0
- package/src/ssr/defaultRenderHandler.ts +11 -0
- package/src/ssr/defaultStreamHandler.ts +12 -0
- package/src/ssr/renderRouterToStream.ts +195 -0
- package/src/ssr/renderRouterToString.ts +58 -0
- package/src/ssr/server.ts +8 -0
- package/src/structuralSharing.ts +41 -0
- package/src/typePrimitives.ts +77 -0
- package/src/useAwaited.ts +7 -2
- package/src/useBlocker.tsrx +2 -1
- package/src/useRouterState.ts +20 -0
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
TanStack Router splits a framework-agnostic core (`@tanstack/router-core` — the
|
|
6
6
|
router, route tree, matching, history, and the reactive store) from a thin React
|
|
7
7
|
binding (`@tanstack/react-router`). Mirroring `@octanejs/tanstack-query`, this package
|
|
8
|
-
re-exports the core **verbatim** and reimplements
|
|
9
|
-
The
|
|
10
|
-
changing the import.
|
|
8
|
+
re-exports the core **verbatim** and reimplements the framework binding on octane's
|
|
9
|
+
hooks. The main runtime surface follows `@tanstack/react-router`, so most router
|
|
10
|
+
code works by changing the import.
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
13
|
import {
|
|
@@ -55,7 +55,7 @@ tree renders **pull-based**: `RouterProvider` renders the first match, and each
|
|
|
55
55
|
component's `<Outlet/>` looks up the next match via `matchContext` — so navigation
|
|
56
56
|
re-renders only the matches that changed.
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## Scope
|
|
59
59
|
|
|
60
60
|
Included: `createRouter`, `createRootRoute`, `createRoute`, `RouterProvider`,
|
|
61
61
|
`Outlet`, `Link`, `Navigate`, `useRouter`, `useRouterState`, `useLocation`,
|
|
@@ -66,7 +66,8 @@ rendering** (`notFoundComponent`/`defaultNotFoundComponent`/`notFoundMode` — a
|
|
|
66
66
|
unknown URL or a loader throwing `notFound()` renders the not-found UI inside the
|
|
67
67
|
layout, per TanStack's fuzzy/root boundary rules), plus the full
|
|
68
68
|
`@tanstack/router-core` re-export (`redirect`, `notFound`, history, search helpers,
|
|
69
|
-
types).
|
|
69
|
+
types). The typed route factories, route-bound hooks, and `Link` surface preserve
|
|
70
|
+
TanStack Router's registered-route inference.
|
|
70
71
|
|
|
71
72
|
The 2026-07-06 gap-closure sweep (see `docs/tanstack-parity-audit.md`) additionally
|
|
72
73
|
landed the full Match pipeline (per-route Suspense/CatchBoundary/CatchNotFound,
|
|
@@ -78,6 +79,21 @@ validation/middleware, and full `Link` parity (preloading, masking,
|
|
|
78
79
|
`activeProps`/`inactiveProps`) — differential-verified byte-equal against the real
|
|
79
80
|
`@tanstack/react-router`.
|
|
80
81
|
|
|
82
|
+
File routing is supported through `createFileRoute` and `createLazyFileRoute`.
|
|
83
|
+
`@octanejs/tanstack-start` consumes the exported
|
|
84
|
+
`@octanejs/tanstack-router/generator-plugin` from its package-owned generator; the
|
|
85
|
+
integration masks native `.tsrx` template bodies without changing source offsets,
|
|
86
|
+
so generated route-tree edits preserve authored Octane route modules.
|
|
87
|
+
|
|
88
|
+
The `@octanejs/tanstack-router/ssr/server` and `/ssr/client` entries provide
|
|
89
|
+
`RouterServer`, `RouterClient`, buffered rendering, and readable-stream rendering.
|
|
90
|
+
Route-owned `Html`/`Head`/`Body`, `HeadContent`, `Scripts`, and `ScriptOnce` preserve
|
|
91
|
+
full-document SSR, head assets, and hydration. Streaming uses Octane's native
|
|
92
|
+
injection source, so the document begins with a doctype, renderer styles are placed
|
|
93
|
+
inside `<head>` with the configured CSP nonce, and serialized router data can stream
|
|
94
|
+
without a byte-level HTML transform. These entries are the router foundation used
|
|
95
|
+
by `@octanejs/tanstack-start`.
|
|
96
|
+
|
|
81
97
|
```tsx
|
|
82
98
|
// streaming a deferred loader value
|
|
83
99
|
<Await promise={data.slow} fallback={'loading…'}>
|
|
@@ -91,10 +107,6 @@ validation/middleware, and full `Link` parity (preloading, masking,
|
|
|
91
107
|
createRoute({ path: 'item/$id', component: lazyRouteComponent(() => import('./Item')) })
|
|
92
108
|
```
|
|
93
109
|
|
|
94
|
-
Deferred: file-based routing + the codegen plugin, devtools, the SSR entries
|
|
95
|
-
(`RouterServer`/`RouterClient`, `HeadContent`/`Scripts`), and the typed public
|
|
96
|
-
surface (factories/hooks are still `any`).
|
|
97
|
-
|
|
98
110
|
Current scope, divergences, and verification status are tracked in the generated
|
|
99
111
|
[bindings status table](../../docs/bindings-status.md) (sourced from this
|
|
100
112
|
package's `status.json`).
|
|
@@ -103,5 +115,6 @@ package's `status.json`).
|
|
|
103
115
|
|
|
104
116
|
- **Refs are props** (octane's model) — `createLink`'s `forwardRef` becomes a `ref`
|
|
105
117
|
prop.
|
|
106
|
-
- **
|
|
107
|
-
|
|
118
|
+
- **Native DOM events** — link callbacks receive browser events rather than React
|
|
119
|
+
synthetic events.
|
|
120
|
+
- Router devtools are distributed separately and are not part of this binding.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -29,30 +29,45 @@
|
|
|
29
29
|
"main": "src/index.ts",
|
|
30
30
|
"module": "src/index.ts",
|
|
31
31
|
"types": "src/index.ts",
|
|
32
|
+
"sideEffects": false,
|
|
32
33
|
"files": [
|
|
33
34
|
"src",
|
|
34
35
|
"README.md"
|
|
35
36
|
],
|
|
36
37
|
"exports": {
|
|
37
38
|
".": "./src/index.ts",
|
|
38
|
-
"./history": "./src/history.ts"
|
|
39
|
+
"./history": "./src/history.ts",
|
|
40
|
+
"./ssr/server": {
|
|
41
|
+
"types": "./src/ssr/server.ts",
|
|
42
|
+
"default": "./src/ssr/server.ts"
|
|
43
|
+
},
|
|
44
|
+
"./ssr/client": {
|
|
45
|
+
"types": "./src/ssr/client.ts",
|
|
46
|
+
"default": "./src/ssr/client.ts"
|
|
47
|
+
},
|
|
48
|
+
"./generator-plugin": {
|
|
49
|
+
"types": "./src/generator-plugin.d.ts",
|
|
50
|
+
"default": "./src/generator-plugin.js"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
39
53
|
},
|
|
40
54
|
"dependencies": {
|
|
41
55
|
"@tanstack/history": "1.162.0",
|
|
42
|
-
"@tanstack/router-core": "1.171.
|
|
43
|
-
"@tanstack/store": "^0.9.3"
|
|
56
|
+
"@tanstack/router-core": "1.171.15",
|
|
57
|
+
"@tanstack/store": "^0.9.3",
|
|
58
|
+
"isbot": "^5.1.22"
|
|
44
59
|
},
|
|
45
60
|
"peerDependencies": {
|
|
46
|
-
"octane": "0.1.
|
|
61
|
+
"octane": "0.1.13"
|
|
47
62
|
},
|
|
48
63
|
"devDependencies": {
|
|
49
|
-
"@tanstack/react-router": "1.170.
|
|
50
|
-
"@tsrx/react": "^0.2.
|
|
64
|
+
"@tanstack/react-router": "1.170.18",
|
|
65
|
+
"@tsrx/react": "^0.2.46",
|
|
51
66
|
"esbuild": "^0.28.1",
|
|
52
|
-
"react": "^19.2.
|
|
53
|
-
"react-dom": "^19.2.
|
|
54
|
-
"vitest": "^4.1.
|
|
55
|
-
"octane": "0.1.
|
|
67
|
+
"react": "^19.2.7",
|
|
68
|
+
"react-dom": "^19.2.7",
|
|
69
|
+
"vitest": "^4.1.10",
|
|
70
|
+
"octane": "0.1.13"
|
|
56
71
|
},
|
|
57
72
|
"scripts": {
|
|
58
73
|
"test": "vitest run"
|
package/src/Asset.tsrx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createElement, useLayoutEffect } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import type { RouterManagedTag } from '@tanstack/router-core';
|
|
4
|
+
import { useRouter } from './context.ts';
|
|
5
|
+
import { useHydrated } from './ClientOnly.tsrx';
|
|
6
|
+
|
|
7
|
+
export type AssetProps =
|
|
8
|
+
RouterManagedTag & {
|
|
9
|
+
assetKey: string;
|
|
10
|
+
target: 'head' | 'body';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const MANAGED_KEY_ATTR = 'data-tsr-managed-key';
|
|
14
|
+
|
|
15
|
+
function attributeName(name: string) {
|
|
16
|
+
switch (name) {
|
|
17
|
+
case 'charSet':
|
|
18
|
+
return 'charset';
|
|
19
|
+
case 'className':
|
|
20
|
+
return 'class';
|
|
21
|
+
case 'crossOrigin':
|
|
22
|
+
return 'crossorigin';
|
|
23
|
+
case 'httpEquiv':
|
|
24
|
+
return 'http-equiv';
|
|
25
|
+
default:
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setAttributes(element: Element, attrs: RouterManagedTag['attrs']) {
|
|
31
|
+
if (!attrs) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
35
|
+
if (
|
|
36
|
+
name === 'suppressHydrationWarning' || value === undefined || value === null ||
|
|
37
|
+
value === false
|
|
38
|
+
) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
element.setAttribute(attributeName(name), value === true ? '' : String(value));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findManagedElement(parent: HTMLElement, key: string) {
|
|
46
|
+
for (const element of parent.querySelectorAll(`[${MANAGED_KEY_ATTR}]`)) {
|
|
47
|
+
if (element.getAttribute(MANAGED_KEY_ATTR) === key) {
|
|
48
|
+
return element;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mountAsset(props: AssetProps) {
|
|
55
|
+
const parent =
|
|
56
|
+
props.target === 'head' ? document.head : document.body;
|
|
57
|
+
const existing = findManagedElement(parent, props.assetKey);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return () => existing.remove();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const element = document.createElement(props.tag);
|
|
63
|
+
element.setAttribute(MANAGED_KEY_ATTR, props.assetKey);
|
|
64
|
+
setAttributes(element, props.attrs);
|
|
65
|
+
if (typeof props.children === 'string') {
|
|
66
|
+
element.textContent = props.children;
|
|
67
|
+
}
|
|
68
|
+
parent.appendChild(element);
|
|
69
|
+
return () => element.remove();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function Asset(props: AssetProps) @{
|
|
73
|
+
const router = useRouter();
|
|
74
|
+
const server = isServer ?? router.isServer;
|
|
75
|
+
const hydrated = useHydrated();
|
|
76
|
+
const renderElement = server || props.target === 'body' && !hydrated;
|
|
77
|
+
|
|
78
|
+
if (!server) {
|
|
79
|
+
useLayoutEffect(() => mountAsset(props), [
|
|
80
|
+
props.assetKey,
|
|
81
|
+
props.attrs,
|
|
82
|
+
props.children,
|
|
83
|
+
props.tag,
|
|
84
|
+
props.target,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@if (renderElement) {
|
|
89
|
+
@switch (props.tag) {
|
|
90
|
+
@case 'title': {
|
|
91
|
+
<title {...props.attrs} data-tsr-managed-key={props.assetKey}>
|
|
92
|
+
{props.children ?? '' as string}
|
|
93
|
+
</title>
|
|
94
|
+
}
|
|
95
|
+
@case 'meta': {
|
|
96
|
+
<meta {...props.attrs} data-tsr-managed-key={props.assetKey} />
|
|
97
|
+
}
|
|
98
|
+
@case 'link': {
|
|
99
|
+
<link {...props.attrs} data-tsr-managed-key={props.assetKey} />
|
|
100
|
+
}
|
|
101
|
+
@case 'style': {
|
|
102
|
+
{createElement('style', {
|
|
103
|
+
...props.attrs,
|
|
104
|
+
'data-tsr-managed-key': props.assetKey,
|
|
105
|
+
dangerouslySetInnerHTML: { __html: props.children ?? '' },
|
|
106
|
+
})}
|
|
107
|
+
}
|
|
108
|
+
@case 'script': {
|
|
109
|
+
<script
|
|
110
|
+
{...props.attrs}
|
|
111
|
+
data-tsr-managed-key={props.assetKey}
|
|
112
|
+
dangerouslySetInnerHTML={{ __html: props.children ?? '' }}
|
|
113
|
+
/>
|
|
114
|
+
}
|
|
115
|
+
@default: {
|
|
116
|
+
<></>
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} @else {
|
|
120
|
+
<></>
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentBody } from 'octane';
|
|
2
|
+
import type { RouterManagedTag } from '@tanstack/router-core';
|
|
3
|
+
|
|
4
|
+
export type AssetProps = RouterManagedTag & {
|
|
5
|
+
assetKey: string;
|
|
6
|
+
target: 'head' | 'body';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export declare const Asset: ComponentBody<AssetProps>;
|
package/src/Await.tsrx
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
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.ts';
|
|
8
9
|
|
|
9
10
|
function AwaitInner(props: { promise: Promise<any>; render: (data: any) => OctaneNode }) {
|
|
10
|
-
const data = use(props.promise);
|
|
11
|
+
const data = use(toExternalHydrationThenable(props.promise));
|
|
11
12
|
return props.render(data);
|
|
12
13
|
}
|
|
13
14
|
|
package/src/Await.tsrx.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { AwaitOptions } from './useAwaited';
|
|
2
|
+
|
|
3
|
+
export declare function Await<T>(
|
|
4
|
+
props: AwaitOptions<T> & {
|
|
5
|
+
children: (data: T) => unknown;
|
|
6
|
+
fallback?: unknown;
|
|
7
|
+
},
|
|
8
|
+
): void;
|
package/src/Body.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { HYDRATION_RANGE_BOUNDARY, createElement } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import { useRouter } from './context';
|
|
4
|
+
import type { ComponentBody } from 'octane';
|
|
5
|
+
|
|
6
|
+
export interface BodyProps {
|
|
7
|
+
children?: unknown;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HydrationRangeOwner: ComponentBody<Pick<BodyProps, 'children'>> = (props) => props.children;
|
|
12
|
+
|
|
13
|
+
(
|
|
14
|
+
HydrationRangeOwner as typeof HydrationRangeOwner & {
|
|
15
|
+
[HYDRATION_RANGE_BOUNDARY]: 'owner';
|
|
16
|
+
}
|
|
17
|
+
)[HYDRATION_RANGE_BOUNDARY] = 'owner';
|
|
18
|
+
|
|
19
|
+
export const Body: ComponentBody<BodyProps> = (props) => {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const server = isServer ?? router.isServer;
|
|
22
|
+
const { children, ...attrs } = props;
|
|
23
|
+
if (server) {
|
|
24
|
+
return createElement(
|
|
25
|
+
'body',
|
|
26
|
+
attrs,
|
|
27
|
+
createElement('div', { id: '__app' }, createElement(HydrationRangeOwner, {}, children)),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return createElement(HydrationRangeOwner, {}, children);
|
|
31
|
+
};
|
package/src/CatchBoundary.tsrx
CHANGED
|
@@ -97,9 +97,7 @@ export function ErrorComponent(props: {
|
|
|
97
97
|
style="font-size:.7em;border:1px solid red;border-radius:.25rem;padding:.3rem;color:red;overflow:auto"
|
|
98
98
|
>
|
|
99
99
|
@if (props.error?.message) {
|
|
100
|
-
<code>
|
|
101
|
-
{props.error.message as string}
|
|
102
|
-
</code>
|
|
100
|
+
<code>{props.error.message as string}</code>
|
|
103
101
|
}
|
|
104
102
|
</pre>
|
|
105
103
|
</div>
|
package/src/ClientOnly.tsrx
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
// call sites).
|
|
8
8
|
import { useSyncExternalStore } from 'octane';
|
|
9
9
|
import type { OctaneNode } from 'octane';
|
|
10
|
+
import { splitSlot, subSlot } from './internal.ts';
|
|
10
11
|
|
|
11
12
|
const subscribe = () => () => {};
|
|
12
13
|
|
|
13
|
-
export function useHydrated() {
|
|
14
|
-
|
|
14
|
+
export function useHydrated(...args: Array<unknown>) {
|
|
15
|
+
const [, slot] = splitSlot(args);
|
|
16
|
+
return useSyncExternalStore(subscribe, () => true, () => false, subSlot(slot, 'hydrated'));
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function ClientOnly(props: { children?: OctaneNode; fallback?: OctaneNode }) @{
|
package/src/Head.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createElement, createPortal } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import { useRouter } from './context';
|
|
4
|
+
import type { ComponentBody } from 'octane';
|
|
5
|
+
|
|
6
|
+
export interface HeadProps {
|
|
7
|
+
children?: unknown;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Head: ComponentBody<HeadProps> = (props) => {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const server = isServer ?? router.isServer;
|
|
14
|
+
const { children, ...attrs } = props;
|
|
15
|
+
if (server) {
|
|
16
|
+
return createElement('head', attrs, children);
|
|
17
|
+
}
|
|
18
|
+
if (typeof document !== 'undefined') {
|
|
19
|
+
return createPortal(children, document.head);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createElement, useEffect } from 'octane';
|
|
2
|
+
import { DEV_STYLES_ATTR } from '@tanstack/router-core';
|
|
3
|
+
import type { AssetCrossOriginConfig } from '@tanstack/router-core';
|
|
4
|
+
import { Asset } from './Asset.tsrx';
|
|
5
|
+
import type { AssetProps } from './Asset.tsrx';
|
|
6
|
+
import { getAssetKey } from './assetKeys.ts';
|
|
7
|
+
import { useTags } from './headContentUtils.ts';
|
|
8
|
+
import { useHydrated } from './ClientOnly.tsrx';
|
|
9
|
+
|
|
10
|
+
export interface HeadContentProps {
|
|
11
|
+
assetCrossOrigin?: AssetCrossOriginConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function HeadContent(props: HeadContentProps) @{
|
|
15
|
+
const tags = useTags(props.assetCrossOrigin);
|
|
16
|
+
const hydrated = useHydrated();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (process.env.NODE_ENV === 'production' || !hydrated) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
document.querySelectorAll(`link[${DEV_STYLES_ATTR}]`).forEach((element) => element.remove());
|
|
23
|
+
}, [hydrated]);
|
|
24
|
+
|
|
25
|
+
const filteredTags =
|
|
26
|
+
process.env.NODE_ENV !== 'production' && hydrated
|
|
27
|
+
? tags.filter((tag) => tag.tag !== 'link' || tag.attrs?.[DEV_STYLES_ATTR] !== true)
|
|
28
|
+
: tags;
|
|
29
|
+
<>
|
|
30
|
+
{filteredTags.map(
|
|
31
|
+
(tag, index) => // `key` is lifted out of props by createElement (React semantics); the
|
|
32
|
+
// explicit type argument admits it alongside the AssetProps shape.
|
|
33
|
+
createElement<AssetProps & { key?: string }>(Asset, {
|
|
34
|
+
...tag,
|
|
35
|
+
assetKey: getAssetKey('head', tag, index),
|
|
36
|
+
target: 'head',
|
|
37
|
+
key: getAssetKey('head', tag, index),
|
|
38
|
+
}),
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentBody } from 'octane';
|
|
2
|
+
import type { AssetCrossOriginConfig } from '@tanstack/router-core';
|
|
3
|
+
|
|
4
|
+
export interface HeadContentProps {
|
|
5
|
+
assetCrossOrigin?: AssetCrossOriginConfig;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export declare const HeadContent: ComponentBody<HeadContentProps>;
|
package/src/Html.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createElement } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import { useRouter } from './context';
|
|
4
|
+
import type { ComponentBody } from 'octane';
|
|
5
|
+
|
|
6
|
+
export interface HtmlProps {
|
|
7
|
+
children?: unknown;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Html: ComponentBody<HtmlProps> = (props) => {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const server = isServer ?? router.isServer;
|
|
14
|
+
const { children, ...attrs } = props;
|
|
15
|
+
if (server) {
|
|
16
|
+
return createElement('html', attrs, children);
|
|
17
|
+
}
|
|
18
|
+
return children;
|
|
19
|
+
};
|
package/src/Link.tsrx
CHANGED
|
@@ -10,13 +10,31 @@
|
|
|
10
10
|
// drops the anchor's disabled attribute; the aria props carry the state).
|
|
11
11
|
import { isChildrenBlock } from 'octane';
|
|
12
12
|
import { useLinkProps } from './link.ts';
|
|
13
|
+
import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
|
|
14
|
+
import type { LinkComponentProps, OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes.ts';
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
export function Link
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
// Keep the generic implementation signature visible to `.tsrx` consumers so
|
|
17
|
+
// route-aware `to`/`params`/`search` inference and native anchor event types are
|
|
18
|
+
// available before declaration emit.
|
|
19
|
+
export function Link<
|
|
20
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
21
|
+
TFrom extends string = string,
|
|
22
|
+
TTo extends string | undefined = undefined,
|
|
23
|
+
TMaskFrom extends string = TFrom,
|
|
24
|
+
TMaskTo extends string = '',
|
|
25
|
+
>(props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) @{
|
|
26
|
+
const { _asChild: AsChild, children, ...rest } = props as
|
|
27
|
+
typeof props & {
|
|
28
|
+
_asChild?: any;
|
|
29
|
+
};
|
|
30
|
+
const linkProps = useLinkProps(
|
|
31
|
+
rest as UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
|
|
32
|
+
) as
|
|
33
|
+
OctaneAnchorProps & {
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
'data-status'?: string;
|
|
36
|
+
'data-transitioning'?: string;
|
|
37
|
+
};
|
|
20
38
|
const { disabled: _disabled, ...aProps } = linkProps;
|
|
21
39
|
|
|
22
40
|
const resolvedChildren =
|
|
@@ -30,6 +48,6 @@ export function Link(props: any) @{
|
|
|
30
48
|
@if (AsChild) {
|
|
31
49
|
<AsChild {...linkProps}>{resolvedChildren}</AsChild>
|
|
32
50
|
} @else {
|
|
33
|
-
<a {...aProps}>{resolvedChildren}</a>
|
|
51
|
+
<a {...aProps as Record<string, any>}>{resolvedChildren}</a>
|
|
34
52
|
}
|
|
35
53
|
}
|
package/src/Link.tsrx.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare const Link: (props: Record<string, unknown>) => unknown;
|
|
1
|
+
import type { LinkComponent } from './linkTypes';
|
|
2
|
+
|
|
3
|
+
export declare const Link: LinkComponent<'a'>;
|
package/src/Match.tsrx
CHANGED
|
@@ -33,13 +33,26 @@ import { RouteNotFound } from './RouteNotFound.tsrx';
|
|
|
33
33
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
34
34
|
import { CatchNotFound } from './not-found.tsrx';
|
|
35
35
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
36
|
+
import { ClientOnly } from './ClientOnly.tsrx';
|
|
37
|
+
import { ScrollRestorationScript } from './scroll-restoration.tsrx';
|
|
36
38
|
|
|
37
39
|
export function Match(props: { matchId: string }) @{
|
|
38
40
|
const router = useRouter();
|
|
39
41
|
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
40
|
-
const resetKey = useStore(router.stores.loadedAt, (l) => l);
|
|
41
|
-
const
|
|
42
|
-
|
|
42
|
+
const resetKey = useStore(router.stores.loadedAt, (l: any) => l as number | string);
|
|
43
|
+
const matchState = useStore(
|
|
44
|
+
matchStore,
|
|
45
|
+
(m: any) => ({
|
|
46
|
+
routeId: m.routeId,
|
|
47
|
+
ssr: m.ssr,
|
|
48
|
+
_displayPending: m._displayPending,
|
|
49
|
+
}),
|
|
50
|
+
(previous, next) =>
|
|
51
|
+
previous.routeId === next.routeId && previous.ssr === next.ssr &&
|
|
52
|
+
previous._displayPending === next._displayPending,
|
|
53
|
+
);
|
|
54
|
+
const routeId = matchState.routeId as string;
|
|
55
|
+
const route = (router.routesById as Record<string, any>)[routeId];
|
|
43
56
|
|
|
44
57
|
const PendingComponent = route.options.pendingComponent ?? router.options.defaultPendingComponent;
|
|
45
58
|
const routeErrorComponent = route.options.errorComponent ?? router.options.defaultErrorComponent;
|
|
@@ -47,13 +60,16 @@ export function Match(props: { matchId: string }) @{
|
|
|
47
60
|
const routeNotFoundComponent = route.isRoot
|
|
48
61
|
? route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component
|
|
49
62
|
: route.options.notFoundComponent;
|
|
63
|
+
const resolvedNoSsr = matchState.ssr === false || matchState.ssr === 'data-only';
|
|
50
64
|
|
|
51
65
|
// Boundary presence, per upstream MatchView. The root route only gets a
|
|
52
66
|
// Suspense boundary when explicitly opted in via wrapInSuspense.
|
|
67
|
+
const suspenseSignal =
|
|
68
|
+
route.options.wrapInSuspense ?? PendingComponent ??
|
|
69
|
+
(routeErrorComponent as ErrorRouteComponent | undefined)?.preload;
|
|
53
70
|
const SuspenseWrap =
|
|
54
|
-
(!route.isRoot || route.options.wrapInSuspense) &&
|
|
55
|
-
(
|
|
56
|
-
(routeErrorComponent as ErrorRouteComponent | undefined)?.preload)
|
|
71
|
+
(!route.isRoot || route.options.wrapInSuspense || resolvedNoSsr) &&
|
|
72
|
+
(suspenseSignal || resolvedNoSsr)
|
|
57
73
|
? Suspense
|
|
58
74
|
: SafeFragment;
|
|
59
75
|
const CatchWrap = routeErrorComponent ? CatchBoundary : SafeFragment;
|
|
@@ -74,7 +90,7 @@ export function Match(props: { matchId: string }) @{
|
|
|
74
90
|
errorComponent={routeErrorComponent || ErrorComponent}
|
|
75
91
|
onCatch={(error: Error, errorInfo: ErrorInfo) => {
|
|
76
92
|
if (isNotFound(error)) {
|
|
77
|
-
error.routeId ??= routeId;
|
|
93
|
+
(error as any).routeId ??= routeId;
|
|
78
94
|
throw error;
|
|
79
95
|
}
|
|
80
96
|
if (routeOnCatch) routeOnCatch(error, errorInfo);
|
|
@@ -82,7 +98,7 @@ export function Match(props: { matchId: string }) @{
|
|
|
82
98
|
>
|
|
83
99
|
<NotFoundWrap
|
|
84
100
|
fallback={(error: NotFoundError) => {
|
|
85
|
-
error.routeId ??= routeId;
|
|
101
|
+
(error as any).routeId ??= routeId;
|
|
86
102
|
|
|
87
103
|
if (
|
|
88
104
|
!routeNotFoundComponent || error.routeId && error.routeId !== routeId ||
|
|
@@ -93,13 +109,24 @@ export function Match(props: { matchId: string }) @{
|
|
|
93
109
|
return createElement(routeNotFoundComponent, error as any);
|
|
94
110
|
}}
|
|
95
111
|
>
|
|
96
|
-
|
|
112
|
+
@if (resolvedNoSsr || matchState._displayPending) {
|
|
113
|
+
<ClientOnly fallback={pendingElement}>
|
|
114
|
+
<MatchInner matchId={props.matchId} />
|
|
115
|
+
</ClientOnly>
|
|
116
|
+
} @else {
|
|
117
|
+
<MatchInner matchId={props.matchId} />
|
|
118
|
+
}
|
|
97
119
|
</NotFoundWrap>
|
|
98
120
|
</CatchWrap>
|
|
99
121
|
</SuspenseWrap>
|
|
100
122
|
</matchContext.Provider>
|
|
101
123
|
@if (parentRouteId === rootRouteId) {
|
|
102
|
-
|
|
124
|
+
<>
|
|
125
|
+
<OnRendered />
|
|
126
|
+
@if (router.options.scrollRestoration) {
|
|
127
|
+
<ScrollRestorationScript />
|
|
128
|
+
}
|
|
129
|
+
</>
|
|
103
130
|
}
|
|
104
131
|
</ShellComponent>
|
|
105
132
|
}
|
|
@@ -107,9 +134,9 @@ export function Match(props: { matchId: string }) @{
|
|
|
107
134
|
function MatchInner(props: { matchId: string }) {
|
|
108
135
|
const router = useRouter();
|
|
109
136
|
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
110
|
-
const match = useStore(matchStore, (m) => m);
|
|
137
|
+
const match = useStore(matchStore, (m: any) => m);
|
|
111
138
|
const routeId = match.routeId as string;
|
|
112
|
-
const route = router.routesById[routeId];
|
|
139
|
+
const route = (router.routesById as Record<string, any>)[routeId];
|
|
113
140
|
|
|
114
141
|
// The live match in the router (if still mounted there) wins over the
|
|
115
142
|
// snapshot for promise lookups, per upstream getMatchPromise.
|
|
@@ -182,7 +209,7 @@ function OnRendered() @{
|
|
|
182
209
|
const prevResolvedLocationRef = useRef<ParsedLocation<any> | undefined>(undefined);
|
|
183
210
|
const renderedLocationKey = useStore(
|
|
184
211
|
router.stores.resolvedLocation,
|
|
185
|
-
(loc) => loc?.state.__TSR_key,
|
|
212
|
+
(loc: ParsedLocation<any> | undefined) => loc?.state.__TSR_key,
|
|
186
213
|
);
|
|
187
214
|
|
|
188
215
|
useLayoutEffect(() => {
|