@octanejs/tanstack-router 0.2.7 → 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
@@ -134,3 +134,71 @@ package's `status.json`).
134
134
  - **Native DOM events** — link callbacks receive browser events rather than React
135
135
  synthetic events.
136
136
  - Router devtools are distributed separately and are not part of this binding.
137
+
138
+ ## Compiled route trees
139
+
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
+
147
+ ```tsx
148
+ import {
149
+ createRootRoute, createRoute, createRouter, createBrowserHistory,
150
+ RouterProvider, Outlet, lazyRouteComponent,
151
+ } from '@octanejs/tanstack-router/compiled';
152
+
153
+ const root = createRootRoute({ component: Layout });
154
+ const item = createRoute({
155
+ getParentRoute: () => root,
156
+ path: 'items/$id',
157
+ component: lazyRouteComponent(() => import('./Item')),
158
+ });
159
+ const router = createRouter({
160
+ routeTree: root.addChildren([item]),
161
+ history: createBrowserHistory(),
162
+ });
163
+
164
+ function Layout() @{
165
+ <main><Outlet /></main>
166
+ }
167
+ function App() @{
168
+ <RouterProvider router={router} />
169
+ }
170
+ ```
171
+
172
+ Import read/navigation hooks (`useMatch`, `useParams`, `useSearch`,
173
+ `useLoaderData`, `useLoaderDeps`, `useRouteContext`, `useLocation`,
174
+ `useMatches`, `useParentMatches`, `useChildMatches`, `useNavigate`,
175
+ `useCanGoBack`, `useRouter`, and `useRouterState`) from the same `/compiled`
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.
203
+ For returned JSX, host strings, renderable wrappers or mixed route trees, retain
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.7",
3
+ "version": "0.2.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -37,6 +37,7 @@
37
37
  "exports": {
38
38
  ".": "./src/index.ts",
39
39
  "./history": "./src/history.ts",
40
+ "./compiled": "./src/compiled.ts",
40
41
  "./ssr/server": {
41
42
  "types": "./src/ssr/server.ts",
42
43
  "default": "./src/ssr/server.ts"
@@ -64,7 +65,7 @@
64
65
  "@tanstack/react-router": "1.170.41",
65
66
  "@tsrx/react": "^0.4.5",
66
67
  "esbuild": "^0.28.1",
67
- "octane": "0.13.0",
68
+ "octane": "0.13.7",
68
69
  "react": "19.2.7",
69
70
  "react-dom": "19.2.7",
70
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();
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);
@@ -19,7 +19,7 @@
19
19
  // restoration, devtools) fire.
20
20
  import { useLayoutEffect, useState, startTransition } from 'octane';
21
21
  import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
22
- import { useRouter } from './context.ts';
22
+ import type { AnyRouter } from '@tanstack/router-core';
23
23
 
24
24
  // Resolve the pending render acknowledgement with whether the expected matches
25
25
  // rendered, then clear the slot. `owner` is `router._rendered`: `[expected, resolve]`.
@@ -29,9 +29,9 @@ export function settleOwner(owner: Array<any>, rendered: boolean) {
29
29
  settle?.(rendered);
30
30
  }
31
31
 
32
- export function useTransitioner() {
33
- const router = useRouter() as any;
34
- const [, forceRender] = useState<object>();
32
+ export function useTransitioner(routerInstance: AnyRouter) {
33
+ const router = routerInstance as any;
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() {
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() {
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
+ };