@octanejs/tanstack-router 0.1.9 → 0.1.11
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 +22 -7
- package/src/Asset.tsrx +121 -0
- package/src/Asset.tsrx.d.ts +9 -0
- package/src/Await.tsrx +9 -3
- package/src/Await.tsrx.d.ts +8 -6
- package/src/Body.ts +31 -0
- package/src/CatchBoundary.tsrx +21 -4
- package/src/CatchBoundary.tsrx.d.ts +10 -4
- package/src/ClientOnly.tsrx +6 -3
- 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 +19 -4
- package/src/Link.tsrx.d.ts +3 -4
- package/src/Match.tsrx +61 -29
- package/src/MatchRoute.tsrx +6 -3
- package/src/Matches.tsrx +7 -7
- package/src/Navigate.tsrx +2 -1
- package/src/Outlet.tsrx +8 -6
- package/src/RouteNotFound.tsrx +2 -2
- package/src/RouterProvider.tsrx +12 -2
- package/src/RouterProvider.tsrx.d.ts +9 -5
- package/src/SafeFragment.tsrx +4 -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 +5 -1
- 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 +100 -0
- package/src/headContentUtils.ts +172 -0
- package/src/hooks.ts +151 -0
- package/src/index.ts +95 -3
- package/src/lazyRouteComponent.ts +2 -1
- package/src/link.ts +25 -4
- package/src/linkTypes.ts +96 -0
- package/src/not-found.tsrx +19 -6
- package/src/not-found.tsrx.d.ts +5 -2
- package/src/octane-compiler.d.ts +12 -0
- package/src/route.ts +473 -36
- 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 +73 -8
- package/src/useBlocker.tsrx.d.ts +58 -2
- package/src/useRouterState.ts +20 -0
- package/src/useStore.ts +17 -6
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.11",
|
|
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.12"
|
|
47
62
|
},
|
|
48
63
|
"devDependencies": {
|
|
49
64
|
"@tanstack/react-router": "1.170.16",
|
|
50
|
-
"@tsrx/react": "^0.2.
|
|
65
|
+
"@tsrx/react": "^0.2.44",
|
|
51
66
|
"esbuild": "^0.28.1",
|
|
52
67
|
"react": "^19.2.0",
|
|
53
68
|
"react-dom": "^19.2.0",
|
|
54
69
|
"vitest": "^4.1.9",
|
|
55
|
-
"octane": "0.1.
|
|
70
|
+
"octane": "0.1.12"
|
|
56
71
|
},
|
|
57
72
|
"scripts": {
|
|
58
73
|
"test": "vitest run"
|
package/src/Asset.tsrx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
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 = RouterManagedTag & {
|
|
8
|
+
assetKey: string;
|
|
9
|
+
target: 'head' | 'body';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MANAGED_KEY_ATTR = 'data-tsr-managed-key';
|
|
13
|
+
|
|
14
|
+
function attributeName(name: string) {
|
|
15
|
+
switch (name) {
|
|
16
|
+
case 'charSet':
|
|
17
|
+
return 'charset';
|
|
18
|
+
case 'className':
|
|
19
|
+
return 'class';
|
|
20
|
+
case 'crossOrigin':
|
|
21
|
+
return 'crossorigin';
|
|
22
|
+
case 'httpEquiv':
|
|
23
|
+
return 'http-equiv';
|
|
24
|
+
default:
|
|
25
|
+
return name;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setAttributes(element: Element, attrs: RouterManagedTag['attrs']) {
|
|
30
|
+
if (!attrs) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
34
|
+
if (
|
|
35
|
+
name === 'suppressHydrationWarning' || value === undefined || value === null ||
|
|
36
|
+
value === false
|
|
37
|
+
) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
element.setAttribute(attributeName(name), value === true ? '' : String(value));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findManagedElement(parent: HTMLElement, key: string) {
|
|
45
|
+
for (const element of parent.querySelectorAll(`[${MANAGED_KEY_ATTR}]`)) {
|
|
46
|
+
if (element.getAttribute(MANAGED_KEY_ATTR) === key) {
|
|
47
|
+
return element;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mountAsset(props: AssetProps) {
|
|
54
|
+
const parent =
|
|
55
|
+
props.target === 'head' ? document.head : document.body;
|
|
56
|
+
const existing = findManagedElement(parent, props.assetKey);
|
|
57
|
+
if (existing) {
|
|
58
|
+
return () => existing.remove();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const element = document.createElement(props.tag);
|
|
62
|
+
element.setAttribute(MANAGED_KEY_ATTR, props.assetKey);
|
|
63
|
+
setAttributes(element, props.attrs);
|
|
64
|
+
if (typeof props.children === 'string') {
|
|
65
|
+
element.textContent = props.children;
|
|
66
|
+
}
|
|
67
|
+
parent.appendChild(element);
|
|
68
|
+
return () => element.remove();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function Asset(props: AssetProps) @{
|
|
72
|
+
const router = useRouter();
|
|
73
|
+
const server = isServer ?? router.isServer;
|
|
74
|
+
const hydrated = useHydrated();
|
|
75
|
+
const renderElement = server || props.target === 'body' && !hydrated;
|
|
76
|
+
|
|
77
|
+
if (!server) {
|
|
78
|
+
useLayoutEffect(() => mountAsset(props), [
|
|
79
|
+
props.assetKey,
|
|
80
|
+
props.attrs,
|
|
81
|
+
props.children,
|
|
82
|
+
props.tag,
|
|
83
|
+
props.target,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@if (renderElement) {
|
|
88
|
+
@switch (props.tag) {
|
|
89
|
+
@case 'title': {
|
|
90
|
+
<title {...props.attrs} data-tsr-managed-key={props.assetKey}>
|
|
91
|
+
{props.children ?? '' as string}
|
|
92
|
+
</title>
|
|
93
|
+
}
|
|
94
|
+
@case 'meta': {
|
|
95
|
+
<meta {...props.attrs} data-tsr-managed-key={props.assetKey} />
|
|
96
|
+
}
|
|
97
|
+
@case 'link': {
|
|
98
|
+
<link {...props.attrs} data-tsr-managed-key={props.assetKey} />
|
|
99
|
+
}
|
|
100
|
+
@case 'style': {
|
|
101
|
+
{createElement('style', {
|
|
102
|
+
...props.attrs,
|
|
103
|
+
'data-tsr-managed-key': props.assetKey,
|
|
104
|
+
dangerouslySetInnerHTML: { __html: props.children ?? '' },
|
|
105
|
+
})}
|
|
106
|
+
}
|
|
107
|
+
@case 'script': {
|
|
108
|
+
<script
|
|
109
|
+
{...props.attrs}
|
|
110
|
+
data-tsr-managed-key={props.assetKey}
|
|
111
|
+
dangerouslySetInnerHTML={{ __html: props.children ?? '' }}
|
|
112
|
+
/>
|
|
113
|
+
}
|
|
114
|
+
@default: {
|
|
115
|
+
<></>
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} @else {
|
|
119
|
+
<></>
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -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
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
// `render` prop (re-passing it as `{children}` would be an identifier, not a literal
|
|
5
5
|
// render-prop, so the consumer's `<Await>{(d) => …}</Await>` stays the public API).
|
|
6
6
|
import { use } from 'octane';
|
|
7
|
+
import type { OctaneNode } from 'octane';
|
|
8
|
+
import { toExternalHydrationThenable } from './externalHydration.ts';
|
|
7
9
|
|
|
8
|
-
function AwaitInner(props) {
|
|
9
|
-
const data = use(props.promise);
|
|
10
|
+
function AwaitInner(props: { promise: Promise<any>; render: (data: any) => OctaneNode }) {
|
|
11
|
+
const data = use(toExternalHydrationThenable(props.promise));
|
|
10
12
|
return props.render(data);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function Await(props
|
|
15
|
+
export function Await(props: {
|
|
16
|
+
promise: Promise<any>;
|
|
17
|
+
fallback?: OctaneNode;
|
|
18
|
+
children: (data: any) => OctaneNode;
|
|
19
|
+
}) @{
|
|
14
20
|
@try {
|
|
15
21
|
<AwaitInner promise={props.promise} render={props.children} />
|
|
16
22
|
} @pending {
|
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
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
// commit later, same observable outcome). Match passes the router's `loadedAt`
|
|
15
15
|
// so navigation clears route error UI.
|
|
16
16
|
import { useState, useRef, useLayoutEffect } from 'octane';
|
|
17
|
+
import type { OctaneNode } from 'octane';
|
|
18
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route.ts';
|
|
17
19
|
|
|
18
|
-
export function CatchBoundary(props
|
|
20
|
+
export function CatchBoundary(props: {
|
|
21
|
+
getResetKey: () => number | string;
|
|
22
|
+
children: OctaneNode;
|
|
23
|
+
errorComponent?: ErrorRouteComponent;
|
|
24
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
25
|
+
}) @{
|
|
19
26
|
@try {
|
|
20
27
|
<>
|
|
21
28
|
{props.children}
|
|
@@ -31,10 +38,16 @@ export function CatchBoundary(props) @{
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
function CatchErrorView(props
|
|
41
|
+
function CatchErrorView(props: {
|
|
42
|
+
error: any;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
getResetKey?: () => number | string;
|
|
45
|
+
errorComponent?: ErrorRouteComponent;
|
|
46
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
47
|
+
}) @{
|
|
35
48
|
// componentDidCatch parity — once per distinct caught error, during render so a
|
|
36
49
|
// rethrow (the notFound-forwarding path) escapes to the parent boundary.
|
|
37
|
-
const reported = useRef(null);
|
|
50
|
+
const reported = useRef<unknown>(null);
|
|
38
51
|
if (reported.current !== props.error) {
|
|
39
52
|
reported.current = props.error;
|
|
40
53
|
if (props.onCatch) props.onCatch(props.error, { componentStack: '' });
|
|
@@ -60,7 +73,11 @@ function CatchErrorView(props) @{
|
|
|
60
73
|
// The default error UI, markup-identical to react-router's ErrorComponent (string
|
|
61
74
|
// styles serialize to the same inline css React produces, so the differential rig
|
|
62
75
|
// can byte-compare it).
|
|
63
|
-
export function ErrorComponent(props
|
|
76
|
+
export function ErrorComponent(props: {
|
|
77
|
+
error: any;
|
|
78
|
+
reset?: () => void;
|
|
79
|
+
info?: { componentStack?: string };
|
|
80
|
+
}) @{
|
|
64
81
|
const [show, setShow] = useState(process.env.NODE_ENV !== 'production');
|
|
65
82
|
|
|
66
83
|
<div style="padding:.5rem;max-width:100%">
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// Type declaration for the .tsrx components (resolved by relative path).
|
|
2
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route';
|
|
3
|
+
|
|
2
4
|
export declare const CatchBoundary: (props: {
|
|
3
|
-
getResetKey
|
|
4
|
-
errorComponent?:
|
|
5
|
-
onCatch?: (error:
|
|
5
|
+
getResetKey: () => number | string;
|
|
6
|
+
errorComponent?: ErrorRouteComponent;
|
|
7
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
6
8
|
children?: unknown;
|
|
7
9
|
}) => unknown;
|
|
8
|
-
export declare const ErrorComponent: (props: {
|
|
10
|
+
export declare const ErrorComponent: (props: {
|
|
11
|
+
error: any;
|
|
12
|
+
reset?: () => void;
|
|
13
|
+
info?: { componentStack?: string };
|
|
14
|
+
}) => unknown;
|
package/src/ClientOnly.tsrx
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
// compiler slots the hook calls (and withSlot-disambiguates useHydrated's
|
|
7
7
|
// call sites).
|
|
8
8
|
import { useSyncExternalStore } from 'octane';
|
|
9
|
+
import type { OctaneNode } from 'octane';
|
|
10
|
+
import { splitSlot, subSlot } from './internal.ts';
|
|
9
11
|
|
|
10
12
|
const subscribe = () => () => {};
|
|
11
13
|
|
|
12
|
-
export function useHydrated() {
|
|
13
|
-
|
|
14
|
+
export function useHydrated(...args: Array<unknown>) {
|
|
15
|
+
const [, slot] = splitSlot(args);
|
|
16
|
+
return useSyncExternalStore(subscribe, () => true, () => false, subSlot(slot, 'hydrated'));
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function ClientOnly(props) @{
|
|
19
|
+
export function ClientOnly(props: { children?: OctaneNode; fallback?: OctaneNode }) @{
|
|
17
20
|
const hydrated = useHydrated();
|
|
18
21
|
<>
|
|
19
22
|
{hydrated ? props.children : props.fallback ?? null}
|
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,10 +10,25 @@
|
|
|
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
|
-
|
|
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<TRouter extends AnyRouter = RegisteredRouter, TFrom extends string = string, TTo extends | string
|
|
20
|
+
| undefined = undefined, TMaskFrom extends string = TFrom, TMaskTo extends string = ''>(props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) @{
|
|
21
|
+
const { _asChild: AsChild, children, ...rest } = props as typeof props & {
|
|
22
|
+
_asChild?: any;
|
|
23
|
+
};
|
|
24
|
+
const linkProps = useLinkProps(
|
|
25
|
+
rest as UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
|
|
26
|
+
) as
|
|
27
|
+
OctaneAnchorProps & {
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
'data-status'?: string;
|
|
30
|
+
'data-transitioning'?: string;
|
|
31
|
+
};
|
|
17
32
|
const { disabled: _disabled, ...aProps } = linkProps;
|
|
18
33
|
|
|
19
34
|
const resolvedChildren =
|
|
@@ -27,6 +42,6 @@ export function Link(props) @{
|
|
|
27
42
|
@if (AsChild) {
|
|
28
43
|
<AsChild {...linkProps}>{resolvedChildren}</AsChild>
|
|
29
44
|
} @else {
|
|
30
|
-
<a {...aProps}>{resolvedChildren}</a>
|
|
45
|
+
<a {...aProps as Record<string, any>}>{resolvedChildren}</a>
|
|
31
46
|
}
|
|
32
47
|
}
|
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'>;
|