@octanejs/tanstack-router 0.2.8 → 0.2.9

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 CHANGED
@@ -135,26 +135,26 @@ package's `status.json`).
135
135
  synthetic events.
136
136
  - Router devtools are distributed separately and are not part of this binding.
137
137
 
138
- ## Compiled code routes
138
+ ## Compiled route trees
139
139
 
140
- Use `@octanejs/tanstack-router/compiled` for a tree whose selected route, pending,
141
- error and not-found components are compiled Octane `@{ ... }` bodies. Its
142
- `CompiledRouterProvider`, `CompiledOutlet`, and `lazyCompiledRouteComponent`
143
- avoid retaining the descriptor renderer merely to select a route at runtime.
144
- TanStack still owns the matching, loading, navigation, history and render
145
- acknowledgements.
140
+ Use `@octanejs/tanstack-router/compiled` for a tree whose selected route, shell,
141
+ pending, error and not-found components are compiled Octane `@{ ... }` bodies.
142
+ The entry exports `RouterProvider`, `Outlet`, and `lazyRouteComponent`; the
143
+ existing `CompiledRouterProvider`, `CompiledOutlet`, and
144
+ `lazyCompiledRouteComponent` names remain aliases. TanStack owns matching,
145
+ loading, navigation, history and render acknowledgements.
146
146
 
147
147
  ```tsx
148
148
  import {
149
- BaseRootRoute, BaseRoute, createRouter, createBrowserHistory,
150
- CompiledRouterProvider, CompiledOutlet, lazyCompiledRouteComponent, useParams,
149
+ createRootRoute, createRoute, createRouter, createBrowserHistory,
150
+ RouterProvider, Outlet, lazyRouteComponent,
151
151
  } from '@octanejs/tanstack-router/compiled';
152
152
 
153
- const root = new BaseRootRoute({ component: Layout });
154
- const item = new BaseRoute({
153
+ const root = createRootRoute({ component: Layout });
154
+ const item = createRoute({
155
155
  getParentRoute: () => root,
156
156
  path: 'items/$id',
157
- component: lazyCompiledRouteComponent(() => import('./Item')),
157
+ component: lazyRouteComponent(() => import('./Item')),
158
158
  });
159
159
  const router = createRouter({
160
160
  routeTree: root.addChildren([item]),
@@ -162,10 +162,10 @@ const router = createRouter({
162
162
  });
163
163
 
164
164
  function Layout() @{
165
- <main><CompiledOutlet /></main>
165
+ <main><Outlet /></main>
166
166
  }
167
167
  function App() @{
168
- <CompiledRouterProvider router={router} />
168
+ <RouterProvider router={router} />
169
169
  }
170
170
  ```
171
171
 
@@ -173,12 +173,32 @@ Import read/navigation hooks (`useMatch`, `useParams`, `useSearch`,
173
173
  `useLoaderData`, `useLoaderDeps`, `useRouteContext`, `useLocation`,
174
174
  `useMatches`, `useParentMatches`, `useChildMatches`, `useNavigate`,
175
175
  `useCanGoBack`, `useRouter`, and `useRouterState`) from the same `/compiled`
176
- entry. It owns a separate pair of non-callable contexts. Base route factories
177
- come directly from router-core: use the free hooks in the route components,
178
- rather than route-bound hooks or `Route.Link` from the normal Octane entry.
179
-
180
- The first compiled contract excludes `Wrap`, `InnerWrap`, and
181
- `shellComponent`; the provider reports a diagnostic if they are configured.
176
+ entry. It owns separate non-callable contexts. Its `createRoute`,
177
+ `createRootRoute`, `createRootRouteWithContext`, `getRouteApi`, `createFileRoute`,
178
+ `createLazyRoute`, and `createLazyFileRoute` attach accessors to those contexts.
179
+ Route-bound hooks and `Route.Link` work alongside the exported `Link`,
180
+ `useLinkProps`, and `createLink`. Factories construct router-core base instances;
181
+ they do not promise `instanceof Route` from the ordinary entry.
182
+
183
+ The compiled entry also exports `Html`, `Head`, `Body`, `HeadContent`, `Scripts`,
184
+ `Asset`, `useTags`, `getAssetKeys`, `ClientOnly`, and `useHydrated` for document
185
+ shells. Root shells can read their root match and context. SSR and client builds
186
+ must select the same entry for a tree. Start applications select this entry with
187
+ `compiled: true` in their Start plugin options, including generated route and
188
+ split-module imports.
189
+
190
+ Compiled selection removes descriptor creation from route selection, bound
191
+ links and document shells, but does not make the entire binding Strong or
192
+ remove the generic renderer. Link retains synchronous `flushSync` so its
193
+ transition cue is already visible in `onBeforeNavigate` callbacks. Both entries
194
+ share a non-Strong script component that renders host descriptors: new inline and
195
+ external scripts execute, and self-removing hydration data scripts leave no
196
+ template row frames behind. Route scripts retain their managed nodes through
197
+ hydration and are replaced with route ownership. The scroll-restoration
198
+ script remains through adoption and is dropped afterward. Error propagation,
199
+ scroll-script hydration compatibility, and lifecycle dependencies retain their
200
+ existing contracts.
201
+
202
+ `Wrap` and `InnerWrap` remain unsupported and produce an explicit diagnostic.
182
203
  For returned JSX, host strings, renderable wrappers or mixed route trees, retain
183
- `RouterProvider`, `Outlet` and `lazyRouteComponent` from the ordinary entry.
184
- SSR and client builds must select the same entry for a tree.
204
+ the ordinary entry. Its public APIs and contexts remain available unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/tanstack-router",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -65,7 +65,7 @@
65
65
  "@tanstack/react-router": "1.170.41",
66
66
  "@tsrx/react": "^0.4.5",
67
67
  "esbuild": "^0.28.1",
68
- "octane": "0.13.6",
68
+ "octane": "0.13.7",
69
69
  "react": "19.2.7",
70
70
  "react-dom": "19.2.7",
71
71
  "vitest": "^4.1.10"
package/src/Asset.tsrx CHANGED
@@ -1,137 +1,5 @@
1
- import { createElement, useLayoutEffect } from 'octane';
2
- import { isServer } from '@tanstack/router-core/isServer';
3
- import type { RouterManagedTag } from '@tanstack/router-core';
4
1
  import { useRouter } from './context.ts';
5
- import { useHydrated } from './ClientOnly.tsrx';
2
+ import { createAsset } from './assetComponent.tsrx';
6
3
 
7
- export type AssetProps = RouterManagedTag & {
8
- assetKey: string;
9
- target: 'head' | 'body';
10
- };
11
-
12
- type ManagedAssetProps = AssetProps & { managedKey?: string };
13
-
14
- const MANAGED_KEY_ATTR = 'data-tsr-managed-key';
15
-
16
- // Elements currently owned by a mounted Asset. A managed key is a 32-bit
17
- // content hash, so a collision must not let a second Asset adopt, and later
18
- // remove, an element another Asset still owns.
19
- const claimedElements = new WeakSet<Element>();
20
-
21
- function attributeName(name: string) {
22
- switch (name) {
23
- case 'charSet':
24
- return 'charset';
25
- case 'className':
26
- return 'class';
27
- case 'crossOrigin':
28
- return 'crossorigin';
29
- case 'httpEquiv':
30
- return 'http-equiv';
31
- default:
32
- return name;
33
- }
34
- }
35
-
36
- function setAttributes(element: Element, attrs: RouterManagedTag['attrs']) {
37
- if (!attrs) {
38
- return;
39
- }
40
- for (const [name, value] of Object.entries(attrs)) {
41
- if (
42
- name === 'suppressHydrationWarning' ||
43
- value === undefined ||
44
- value === null ||
45
- value === false
46
- ) {
47
- continue;
48
- }
49
- element.setAttribute(attributeName(name), value === true ? '' : String(value));
50
- }
51
- }
52
-
53
- function findManagedElement(parent: HTMLElement, key: string) {
54
- for (const element of parent.querySelectorAll(`[${MANAGED_KEY_ATTR}]`)) {
55
- if (element.getAttribute(MANAGED_KEY_ATTR) === key && !claimedElements.has(element)) {
56
- return element;
57
- }
58
- }
59
- return undefined;
60
- }
61
-
62
- function claim(element: Element) {
63
- claimedElements.add(element);
64
- return () => {
65
- claimedElements.delete(element);
66
- element.remove();
67
- };
68
- }
69
-
70
- function mountAsset(props: ManagedAssetProps) {
71
- const parent = props.target === 'head' ? document.head : document.body;
72
- const managedKey = props.managedKey ?? props.assetKey;
73
- const existing = findManagedElement(parent, managedKey);
74
- if (existing) {
75
- return claim(existing);
76
- }
77
-
78
- const element = document.createElement(props.tag);
79
- element.setAttribute(MANAGED_KEY_ATTR, managedKey);
80
- setAttributes(element, props.attrs);
81
- if (typeof props.children === 'string') {
82
- element.textContent = props.children;
83
- }
84
- parent.appendChild(element);
85
- return claim(element);
86
- }
87
-
88
- export function Asset(props: AssetProps) @{
89
- const router = useRouter();
90
- const hydrated = useHydrated();
91
- const managedKey = (props as ManagedAssetProps).managedKey ?? props.assetKey;
92
-
93
- if (!(isServer ?? router.isServer)) {
94
- // Compare attrs by value: a rebuilt tag list allocates new attrs objects on
95
- // every navigation, and an identity dependency would remove and re-add
96
- // unchanged assets such as the stylesheet.
97
- const attrsKey = JSON.stringify(props.attrs ?? null);
98
- useLayoutEffect(
99
- () => mountAsset(props),
100
- [props.assetKey, managedKey, props.tag, attrsKey, props.children, props.target],
101
- );
102
- }
103
-
104
- const visibleTag =
105
- (isServer ?? router.isServer) || (props.target === 'body' && !hydrated) ? props.tag : undefined;
106
-
107
- @switch (visibleTag) {
108
- @case 'title': {
109
- <title {...props.attrs} data-tsr-managed-key={managedKey}>
110
- {props.children ?? ('' as string)}
111
- </title>
112
- }
113
- @case 'meta': {
114
- <meta {...props.attrs} data-tsr-managed-key={managedKey} />
115
- }
116
- @case 'link': {
117
- <link {...props.attrs} data-tsr-managed-key={managedKey} />
118
- }
119
- @case 'style': {
120
- {createElement('style', {
121
- ...props.attrs,
122
- 'data-tsr-managed-key': managedKey,
123
- dangerouslySetInnerHTML: { __html: props.children ?? '' },
124
- })}
125
- }
126
- @case 'script': {
127
- <script
128
- {...props.attrs}
129
- data-tsr-managed-key={managedKey}
130
- dangerouslySetInnerHTML={{ __html: props.children ?? '' }}
131
- />
132
- }
133
- @default: {
134
- <></>
135
- }
136
- }
137
- }
4
+ export const Asset = createAsset(useRouter);
5
+ export type { AssetProps } from './assetComponent.tsrx';
@@ -1,3 +1,5 @@
1
+ 'use strong';
2
+
1
3
  // ClientOnly / useHydrated — port of react-router's ClientOnly.tsx. Renders
2
4
  // children only once the client has hydrated: `useHydrated` reads a constant
3
5
  // external store whose server snapshot is `false` and client snapshot is
package/src/Link.tsrx CHANGED
@@ -1,55 +1,4 @@
1
- // A navigating anchor — a thin shell over `useLinkProps` (link.ts), which
2
- // carries ALL of react-router Link's behavior: href building (mask-aware),
3
- // external-URL + dangerous-protocol handling, active-state detection,
4
- // preloading (intent/viewport/render), and the navigate click handler with
5
- // replace/resetScroll/hashScrollIntoView/viewTransition/startTransition/
6
- // ignoreBlocker forwarded. Children may be a render prop —
7
- // `{({ isActive, isTransitioning }) => …}` — and `createLink` renders a custom
8
- // component via the internal `_asChild` prop. Ref is a prop (octane's model —
9
- // no forwardRef); `disabled` renders without an href + role="link" (upstream
10
- // drops the anchor's disabled attribute; the aria props carry the state).
11
- import { isChildrenBlock } from 'octane';
12
1
  import { useLinkProps } from './link.ts';
13
- import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
14
- import type { LinkComponentProps, OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes.ts';
2
+ import { createLinkComponent } from './linkComponent.tsrx';
15
3
 
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 {
27
- _asChild: AsChild,
28
- children,
29
- ...rest
30
- } = props as typeof props & {
31
- _asChild?: any;
32
- };
33
- const linkProps = useLinkProps(
34
- rest as UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
35
- ) as OctaneAnchorProps & {
36
- disabled?: boolean;
37
- 'data-status'?: string;
38
- 'data-transitioning'?: string;
39
- };
40
- const { disabled: _disabled, ...aProps } = linkProps;
41
-
42
- const resolvedChildren =
43
- typeof children === 'function' && !isChildrenBlock(children)
44
- ? children({
45
- isActive: linkProps['data-status'] === 'active',
46
- isTransitioning: linkProps['data-transitioning'] === 'transitioning',
47
- })
48
- : children;
49
-
50
- @if (AsChild) {
51
- <AsChild {...linkProps}>{resolvedChildren}</AsChild>
52
- } @else {
53
- <a {...(aProps as Record<string, any>)}>{resolvedChildren}</a>
54
- }
55
- }
4
+ export const Link = createLinkComponent(useLinkProps);
package/src/Matches.tsrx CHANGED
@@ -32,16 +32,16 @@ export function Matches() @{
32
32
  useLayoutEffect(() => {
33
33
  if (router.options.scrollRestoration) setupScrollRestoration(router);
34
34
  }, [router]);
35
- useTransitioner(router);
35
+ const expectedMatches = useTransitioner(router);
36
36
 
37
37
  <InnerWrap>
38
38
  <Suspense fallback={pendingElement}>
39
- <MatchesInner />
39
+ <MatchesInner expectedMatches={expectedMatches} />
40
40
  </Suspense>
41
41
  </InnerWrap>
42
42
  }
43
43
 
44
- function MatchesInner() @{
44
+ function MatchesInner(props: { expectedMatches: unknown }) @{
45
45
  const router = useRouter();
46
46
  // router-core 1.171.34 dropped the `firstId`/`loadedAt` stores: the root match
47
47
  // is `matches[0]` and the global error boundary resets on its identity. While a
@@ -49,7 +49,9 @@ function MatchesInner() @{
49
49
  // (`acknowledgement[0]`) so the committed tree equals what it awaits, then
50
50
  // settle that render acknowledgement from a layout effect once it lands.
51
51
  const acknowledgement: Array<any> = ((router as any)._rendered ??= []);
52
- const matches = useStore(router.stores.matches, (m: any) => acknowledgement[0] ?? m);
52
+ const matches = useStore(router.stores.matches, (m: any) =>
53
+ acknowledgement[0] === props.expectedMatches ? (props.expectedMatches ?? m) : m,
54
+ ) as any;
53
55
  const rootMatch = matches[0];
54
56
  const routeId = rootMatch?.routeId as string | undefined;
55
57
  const CatchWrap = router.options.disableGlobalCatchBoundary ? SafeFragment : CatchBoundary;
package/src/Scripts.tsrx CHANGED
@@ -1,6 +1,4 @@
1
- import { createElement, useId } from 'octane';
2
- import type { RouterManagedTag } from '@tanstack/router-core';
3
- import { getAssetKeys } from './assetKeys.ts';
1
+ import { createScripts } from './scriptComponent.tsrx';
4
2
  import { useScripts } from './scriptContentUtils.ts';
5
3
 
6
4
  // router-core 1.171.34 owns the hydration-script lifecycle: the server appends
@@ -11,19 +9,4 @@ import { useScripts } from './scriptContentUtils.ts';
11
9
  // the client renders — so there is no client-only barrier to prepend (the former
12
10
  // `$tsr-stream-barrier` belonged to the removed buffered-script model and left a
13
11
  // client <script> the server never rendered).
14
- export function Scripts() @{
15
- const owner = useId();
16
- const scripts: Array<RouterManagedTag> = useScripts();
17
- const keys = getAssetKeys('body', owner, scripts);
18
- <>
19
- {scripts.map((script, index) => {
20
- const { assetKey, managedKey } = keys[index]!;
21
- return createElement('script', {
22
- ...script.attrs,
23
- key: assetKey,
24
- 'data-tsr-managed-key': managedKey,
25
- dangerouslySetInnerHTML: { __html: script.children ?? '' },
26
- });
27
- })}
28
- </>
29
- }
12
+ export const Scripts = createScripts(useScripts);
@@ -31,7 +31,7 @@ export function settleOwner(owner: Array<any>, rendered: boolean) {
31
31
 
32
32
  export function useTransitioner(routerInstance: AnyRouter) {
33
33
  const router = routerInstance as any;
34
- const [, forceRender] = useState<object>();
34
+ const [expectedMatches, setExpectedMatches] = useState<unknown>();
35
35
 
36
36
  const acknowledgement: Array<any> = (router._rendered ??= []);
37
37
 
@@ -40,9 +40,9 @@ export function useTransitioner(routerInstance: AnyRouter) {
40
40
  // A newer transition supersedes an unrendered older one.
41
41
  settleOwner(acknowledgement, false);
42
42
  acknowledgement.push(expected, resolve);
43
- // Force `MatchesInner` to re-read: it renders `acknowledgement[0]` and
44
- // settles this promise from a layout effect once that commit lands.
45
- forceRender({});
43
+ // Publish the matches this transaction needs. MatchesInner consumes this
44
+ // snapshot and acknowledges it only after that exact tree commits.
45
+ setExpectedMatches(expected);
46
46
  startTransition(fn);
47
47
  });
48
48
 
@@ -93,4 +93,5 @@ export function useTransitioner(routerInstance: AnyRouter) {
93
93
  }
94
94
  return unsub;
95
95
  }, [router, router.history]);
96
+ return expectedMatches;
96
97
  }
@@ -0,0 +1,146 @@
1
+ 'use strong';
2
+
3
+ import { useLayoutEffect, useMemo, trustHTML } from 'octane';
4
+ import { isServer } from '@tanstack/router-core/isServer';
5
+ import type { RouterManagedTag } from '@tanstack/router-core';
6
+ import type { useRouter as UseRouter } from './context.ts';
7
+ import { useHydrated } from './ClientOnly.tsrx';
8
+
9
+ export type AssetProps = RouterManagedTag & {
10
+ assetKey: string;
11
+ target: 'head' | 'body';
12
+ };
13
+
14
+ type ManagedAssetProps = AssetProps & { managedKey?: string };
15
+
16
+ const MANAGED_KEY_ATTR = 'data-tsr-managed-key';
17
+
18
+ // Elements currently owned by a mounted Asset. A managed key is a 32-bit
19
+ // content hash, so a collision must not let a second Asset adopt, and later
20
+ // remove, an element another Asset still owns.
21
+ const claimedElements = new WeakSet<Element>();
22
+
23
+ function attributeName(name: string) {
24
+ switch (name) {
25
+ case 'charSet':
26
+ return 'charset';
27
+ case 'className':
28
+ return 'class';
29
+ case 'crossOrigin':
30
+ return 'crossorigin';
31
+ case 'httpEquiv':
32
+ return 'http-equiv';
33
+ default:
34
+ return name;
35
+ }
36
+ }
37
+
38
+ function setAttributes(element: Element, attrs: RouterManagedTag['attrs']) {
39
+ if (!attrs) {
40
+ return;
41
+ }
42
+ for (const [name, value] of Object.entries(attrs)) {
43
+ if (
44
+ name === 'suppressHydrationWarning' ||
45
+ value === undefined ||
46
+ value === null ||
47
+ value === false
48
+ ) {
49
+ continue;
50
+ }
51
+ element.setAttribute(attributeName(name), value === true ? '' : String(value));
52
+ }
53
+ }
54
+
55
+ function findManagedElement(parent: HTMLElement, key: string) {
56
+ for (const element of parent.querySelectorAll(`[${MANAGED_KEY_ATTR}]`)) {
57
+ if (element.getAttribute(MANAGED_KEY_ATTR) === key && !claimedElements.has(element)) {
58
+ return element;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ function claim(element: Element) {
65
+ claimedElements.add(element);
66
+ return () => {
67
+ claimedElements.delete(element);
68
+ element.remove();
69
+ };
70
+ }
71
+
72
+ function mountAsset(props: ManagedAssetProps) {
73
+ const parent = props.target === 'head' ? document.head : document.body;
74
+ const managedKey = props.managedKey ?? props.assetKey;
75
+ const existing = findManagedElement(parent, managedKey);
76
+ if (existing) {
77
+ return claim(existing);
78
+ }
79
+
80
+ const element = document.createElement(props.tag);
81
+ element.setAttribute(MANAGED_KEY_ATTR, managedKey);
82
+ setAttributes(element, props.attrs);
83
+ if (typeof props.children === 'string') {
84
+ element.textContent = props.children;
85
+ }
86
+ parent.appendChild(element);
87
+ return claim(element);
88
+ }
89
+
90
+ export function createAsset(useRouter: typeof UseRouter) {
91
+ function Asset(props: AssetProps) @{
92
+ const router = useRouter();
93
+ const hydrated = useHydrated();
94
+ const managedKey = (props as ManagedAssetProps).managedKey ?? props.assetKey;
95
+
96
+ if (!(isServer ?? router.isServer)) {
97
+ // Compare attrs by value: a rebuilt tag list allocates new attrs objects on
98
+ // every navigation, and an identity dependency would remove and re-add
99
+ // unchanged assets such as the stylesheet.
100
+ const attrsKey = JSON.stringify(props.attrs ?? null);
101
+ const asset = useMemo(
102
+ () => props,
103
+ [props.assetKey, managedKey, props.tag, attrsKey, props.children, props.target],
104
+ );
105
+ useLayoutEffect(() => mountAsset(asset));
106
+ }
107
+
108
+ const visibleTag =
109
+ (isServer ?? router.isServer) || (props.target === 'body' && !hydrated)
110
+ ? props.tag
111
+ : undefined;
112
+
113
+ @switch (visibleTag) {
114
+ @case 'title': {
115
+ <title {...props.attrs} data-tsr-managed-key={managedKey}>
116
+ {props.children ?? ('' as string)}
117
+ </title>
118
+ }
119
+ @case 'meta': {
120
+ <meta {...props.attrs} data-tsr-managed-key={managedKey} />
121
+ }
122
+ @case 'link': {
123
+ <link {...props.attrs} data-tsr-managed-key={managedKey} />
124
+ }
125
+ @case 'style': {
126
+ <style
127
+ {...props.attrs}
128
+ data-tsr-managed-key={managedKey}
129
+ dangerouslySetInnerHTML={trustHTML(props.children ?? '')}
130
+ />
131
+ }
132
+ @case 'script': {
133
+ <script
134
+ {...props.attrs}
135
+ data-tsr-managed-key={managedKey}
136
+ dangerouslySetInnerHTML={trustHTML(props.children ?? '')}
137
+ />
138
+ }
139
+ @default: {
140
+ <></>
141
+ }
142
+ }
143
+ }
144
+
145
+ return Asset;
146
+ }
@@ -0,0 +1,101 @@
1
+ 'use strong';
2
+
3
+ import { useEffect, useId, HYDRATION_RANGE_BOUNDARY, createPortal, useState } from 'octane';
4
+ import { DEV_STYLES_ATTR } from '@tanstack/router-core';
5
+ import type { AssetCrossOriginConfig } from '@tanstack/router-core';
6
+ import { createAsset } from '../assetComponent.tsrx';
7
+ import { createUseTags } from '../headContentBinding';
8
+ import { createUseScripts } from '../scriptContentBinding';
9
+ import { createScripts } from '../scriptComponent.tsrx';
10
+ import { getAssetKeys } from '../assetKeys';
11
+ import { useHydrated } from '../ClientOnly.tsrx';
12
+ import { useRouter } from './bindings';
13
+
14
+ import { isServer } from '@tanstack/router-core/isServer';
15
+
16
+ import type { HtmlProps } from '../Html';
17
+ import type { HeadProps } from '../Head';
18
+ import type { BodyProps } from '../Body';
19
+
20
+ // This boundary returns the authored children directly, as the ordinary Body
21
+ // owner does. Adding a template frame here changes the SSR range it claims.
22
+ function HydrationRangeOwner(props: Pick<BodyProps, 'children'>) {
23
+ return props.children;
24
+ }
25
+
26
+ (
27
+ HydrationRangeOwner as typeof HydrationRangeOwner & {
28
+ [HYDRATION_RANGE_BOUNDARY]: 'owner';
29
+ }
30
+ )[HYDRATION_RANGE_BOUNDARY] = 'owner';
31
+
32
+ // Preserve the ordinary document and hydration-range ownership. Start owns
33
+ // the outer passthrough boundary; this entry reads the compiled router context.
34
+ export function Html(props: HtmlProps) @{
35
+ const router = useRouter();
36
+ const { children, ...attrs } = props;
37
+ @if (isServer ?? router.isServer) {
38
+ <html {...attrs}>{children}</html>
39
+ } @else {
40
+ <>{children}</>
41
+ }
42
+ }
43
+
44
+ export function Head(props: HeadProps) @{
45
+ const router = useRouter();
46
+ const { children, ...attrs } = props;
47
+ @if (isServer ?? router.isServer) {
48
+ <head {...attrs}>{children}</head>
49
+ } @else {
50
+ // Capture the portal target once, before client adoption. This is a
51
+ // browser handle, not state copied from a post-hydration effect.
52
+ const [target] = useState(() => (typeof document === 'undefined' ? null : document.head));
53
+ @if (target) {
54
+ <>
55
+ {createPortal(() => @{
56
+ <>{children}</>
57
+ }, target)}
58
+ </>
59
+ }
60
+ }
61
+ }
62
+
63
+ export function Body(props: BodyProps) @{
64
+ const router = useRouter();
65
+ const { children, ...attrs } = props;
66
+ @if (isServer ?? router.isServer) {
67
+ <body {...attrs}>
68
+ <div id="__app">
69
+ <HydrationRangeOwner children={children} />
70
+ </div>
71
+ </body>
72
+ } @else {
73
+ <HydrationRangeOwner children={children} />
74
+ }
75
+ }
76
+ export const Asset = createAsset(useRouter);
77
+ export const useTags = createUseTags(useRouter);
78
+ const useScripts = createUseScripts(useRouter);
79
+ export const Scripts = createScripts(useScripts);
80
+
81
+ export function HeadContent(props: { assetCrossOrigin?: AssetCrossOriginConfig }) @{
82
+ const owner = useId();
83
+ const tags = useTags(props.assetCrossOrigin);
84
+ const hydrated = useHydrated();
85
+ useEffect(() => {
86
+ if (process.env.NODE_ENV !== 'production' && hydrated) {
87
+ document.querySelectorAll(`link[${DEV_STYLES_ATTR}]`).forEach((element) => element.remove());
88
+ }
89
+ });
90
+ const filtered =
91
+ process.env.NODE_ENV !== 'production' && hydrated
92
+ ? tags.filter((tag) => tag.tag !== 'link' || tag.attrs?.[DEV_STYLES_ATTR] !== true)
93
+ : tags;
94
+ const keys = getAssetKeys('head', owner, filtered);
95
+ const assets = filtered.map((tag, index) => ({ ...tag, ...keys[index]! }));
96
+ <>
97
+ @for (const asset of assets; key asset.assetKey) {
98
+ <Asset {...asset} target="head" />
99
+ }
100
+ </>
101
+ }
@@ -0,0 +1,18 @@
1
+ 'use strong';
2
+
3
+ import { createUseLinkProps } from '../linkBinding';
4
+ import { createLinkComponent } from '../linkComponent.tsrx';
5
+ import { useRouter } from './bindings';
6
+ import type { createLink as StandardCreateLink } from '../link';
7
+
8
+ // The shared hook intentionally retains flushSync's synchronous navigation cue.
9
+ // The compiled entry does not relax its observable event timing.
10
+ export const useLinkProps = createUseLinkProps(useRouter);
11
+ export const Link = createLinkComponent(useLinkProps);
12
+
13
+ export const createLink: typeof StandardCreateLink = (Component: any) => {
14
+ function CreatedLink(props: any) @{
15
+ <Link {...props} _asChild={Component} />
16
+ }
17
+ return CreatedLink as any;
18
+ };