@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.
Files changed (69) hide show
  1. package/README.md +24 -11
  2. package/package.json +22 -7
  3. package/src/Asset.tsrx +121 -0
  4. package/src/Asset.tsrx.d.ts +9 -0
  5. package/src/Await.tsrx +9 -3
  6. package/src/Await.tsrx.d.ts +8 -6
  7. package/src/Body.ts +31 -0
  8. package/src/CatchBoundary.tsrx +21 -4
  9. package/src/CatchBoundary.tsrx.d.ts +10 -4
  10. package/src/ClientOnly.tsrx +6 -3
  11. package/src/Head.ts +22 -0
  12. package/src/HeadContent.tsrx +41 -0
  13. package/src/HeadContent.tsrx.d.ts +8 -0
  14. package/src/Html.ts +19 -0
  15. package/src/Link.tsrx +19 -4
  16. package/src/Link.tsrx.d.ts +3 -4
  17. package/src/Match.tsrx +61 -29
  18. package/src/MatchRoute.tsrx +6 -3
  19. package/src/Matches.tsrx +7 -7
  20. package/src/Navigate.tsrx +2 -1
  21. package/src/Outlet.tsrx +8 -6
  22. package/src/RouteNotFound.tsrx +2 -2
  23. package/src/RouterProvider.tsrx +12 -2
  24. package/src/RouterProvider.tsrx.d.ts +9 -5
  25. package/src/SafeFragment.tsrx +4 -1
  26. package/src/ScriptOnce.tsrx +23 -0
  27. package/src/ScriptOnce.tsrx.d.ts +3 -0
  28. package/src/Scripts.tsrx +42 -0
  29. package/src/Scripts.tsrx.d.ts +3 -0
  30. package/src/Transitioner.tsrx +15 -13
  31. package/src/assetKeys.ts +11 -0
  32. package/src/context.ts +5 -1
  33. package/src/externalHydration.ts +77 -0
  34. package/src/fileRoute.ts +277 -0
  35. package/src/frameworkTypes.ts +42 -0
  36. package/src/generator-plugin.d.ts +20 -0
  37. package/src/generator-plugin.js +100 -0
  38. package/src/headContentUtils.ts +172 -0
  39. package/src/hooks.ts +151 -0
  40. package/src/index.ts +95 -3
  41. package/src/lazyRouteComponent.ts +2 -1
  42. package/src/link.ts +25 -4
  43. package/src/linkTypes.ts +96 -0
  44. package/src/not-found.tsrx +19 -6
  45. package/src/not-found.tsrx.d.ts +5 -2
  46. package/src/octane-compiler.d.ts +12 -0
  47. package/src/route.ts +473 -36
  48. package/src/routeHookTypes.ts +228 -0
  49. package/src/router.ts +31 -6
  50. package/src/scriptContentUtils.ts +64 -0
  51. package/src/scroll-restoration.tsrx +16 -0
  52. package/src/scroll-restoration.tsrx.d.ts +3 -0
  53. package/src/ssr/RouterClient.tsrx +36 -0
  54. package/src/ssr/RouterClient.tsrx.d.ts +4 -0
  55. package/src/ssr/RouterServer.tsrx +6 -0
  56. package/src/ssr/RouterServer.tsrx.d.ts +4 -0
  57. package/src/ssr/client.ts +4 -0
  58. package/src/ssr/defaultRenderHandler.ts +11 -0
  59. package/src/ssr/defaultStreamHandler.ts +12 -0
  60. package/src/ssr/renderRouterToStream.ts +195 -0
  61. package/src/ssr/renderRouterToString.ts +58 -0
  62. package/src/ssr/server.ts +8 -0
  63. package/src/structuralSharing.ts +41 -0
  64. package/src/typePrimitives.ts +77 -0
  65. package/src/useAwaited.ts +7 -2
  66. package/src/useBlocker.tsrx +73 -8
  67. package/src/useBlocker.tsrx.d.ts +58 -2
  68. package/src/useRouterState.ts +20 -0
  69. 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 only the binding on octane's hooks.
9
- The public surface matches `@tanstack/react-router`, so most router code works by
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
- ## v1 scope
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
- - **No `flushSync`** in the `Link` click handler (the one hard `react-dom`
107
- coupling) — navigation state updates run synchronously in v1.
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.9",
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.13",
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.10"
61
+ "octane": "0.1.12"
47
62
  },
48
63
  "devDependencies": {
49
64
  "@tanstack/react-router": "1.170.16",
50
- "@tsrx/react": "^0.2.37",
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.10"
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 {
@@ -1,6 +1,8 @@
1
- // Type declaration for the .tsrx component (resolved by relative path).
2
- export declare const Await: (props: {
3
- promise: any;
4
- children: (data: any) => unknown;
5
- fallback?: unknown;
6
- }) => unknown;
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
+ };
@@ -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?: () => number | string;
4
- errorComponent?: unknown;
5
- onCatch?: (error: any, errorInfo: { componentStack: string }) => void;
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: { error: any; reset?: () => void }) => unknown;
10
+ export declare const ErrorComponent: (props: {
11
+ error: any;
12
+ reset?: () => void;
13
+ info?: { componentStack?: string };
14
+ }) => unknown;
@@ -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
- return useSyncExternalStore(subscribe, () => true, () => false);
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
- export function Link(props) @{
15
- const { _asChild: AsChild, children, ...rest } = props;
16
- const linkProps = useLinkProps(rest);
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
  }
@@ -1,4 +1,3 @@
1
- // Type declaration for the .tsrx component (resolved by relative path). Props are
2
- // permissive for v1 (the full type-safe `to`/`params`/`search` surface from
3
- // react-router's `LinkProps` is a follow-up).
4
- export declare const Link: (props: Record<string, unknown>) => unknown;
1
+ import type { LinkComponent } from './linkTypes';
2
+
3
+ export declare const Link: LinkComponent<'a'>;