@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.
@@ -0,0 +1,391 @@
1
+ // Shared useLinkProps behavior — port of react-router's link.tsx
2
+ // (client path; SSR link rendering arrives with the SSR entries). All of Link's
3
+ // behavior lives here so both `<Link>` and custom `createLink` components share
4
+ // it: href building (mask-aware via publicHref), external-URL detection with
5
+ // dangerous-protocol blocking, active-state detection (exactPathTest /
6
+ // trailing-slash-aware fuzzy prefix + deepEqual partial search match),
7
+ // preloading ('intent' with delay + touchstart/focus, 'viewport' via
8
+ // IntersectionObserver, 'render' once on mount), the click handler (navigate
9
+ // with replace/resetScroll/hashScrollIntoView/viewTransition/startTransition/
10
+ // ignoreBlocker forwarded), and data-status/data-transitioning reflection.
11
+ import { useRef, useState, useEffect, useCallback, flushSync } from 'octane';
12
+ import {
13
+ deepEqual,
14
+ exactPathTest,
15
+ functionalUpdate,
16
+ isDangerousProtocol,
17
+ preloadWarning,
18
+ removeTrailingSlash,
19
+ } from '@tanstack/router-core';
20
+ import type { useRouter as UseRouter } from './context';
21
+ import { useStore } from './useStore';
22
+ import { splitSlot, subSlot } from './internal';
23
+ import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
24
+ import type { OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes';
25
+
26
+ const STATIC_EMPTY_OBJECT = {};
27
+ const STATIC_ACTIVE_OBJECT = { class: 'active' };
28
+ const STATIC_DISABLED_PROPS = { role: 'link', 'aria-disabled': true };
29
+ const STATIC_ACTIVE_PROPS = { 'data-status': 'active', 'aria-current': 'page' };
30
+ const STATIC_TRANSITIONING_PROPS = { 'data-transitioning': 'transitioning' };
31
+
32
+ const timeoutMap = new WeakMap<EventTarget, ReturnType<typeof setTimeout>>();
33
+
34
+ const composeHandlers = (handlers: Array<undefined | ((e: any) => void)>) => (e: Event) => {
35
+ for (const handler of handlers) {
36
+ if (!handler) continue;
37
+ if (e.defaultPrevented) return;
38
+ handler(e);
39
+ }
40
+ };
41
+
42
+ function getHrefOption(
43
+ publicHref: string,
44
+ external: boolean,
45
+ history: any,
46
+ disabled: boolean | undefined,
47
+ ): { href: string; external: boolean } | undefined {
48
+ if (disabled) return undefined;
49
+ // Full URL means a rewrite changed the origin — treat as external-like.
50
+ if (external) return { href: publicHref, external: true };
51
+ return { href: history.createHref(publicHref) || '/', external: false };
52
+ }
53
+
54
+ function isSafeInternal(to: unknown): boolean {
55
+ if (typeof to !== 'string') return false;
56
+ const zero = to.charCodeAt(0);
57
+ if (zero === 47) return to.charCodeAt(1) !== 47; // '/' but not '//'
58
+ return zero === 46; // '.', '..', './', '../'
59
+ }
60
+
61
+ function isCtrlEvent(e: MouseEvent): boolean {
62
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
63
+ }
64
+
65
+ // Merge base/active/inactive styles. Objects merge like upstream; if any is a
66
+ // string the parts join with ';' (octane host styles accept both forms).
67
+ function mergeStyles(base: any, active: any, inactive: any): any {
68
+ if (!base && !active && !inactive) return undefined;
69
+ const parts = [base, active, inactive].filter(Boolean);
70
+ if (parts.length === 1) return parts[0];
71
+ if (parts.some((p) => typeof p === 'string')) {
72
+ return parts
73
+ .map((p) =>
74
+ typeof p === 'string'
75
+ ? p.replace(/;\s*$/, '')
76
+ : Object.entries(p)
77
+ .map(([k, v]) => `${k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())}:${v}`)
78
+ .join(';'),
79
+ )
80
+ .join(';');
81
+ }
82
+ return Object.assign({}, ...parts);
83
+ }
84
+
85
+ export function createUseLinkProps(useRouter: typeof UseRouter) {
86
+ function useLinkProps<
87
+ TRouter extends AnyRouter = RegisteredRouter,
88
+ const TFrom extends string = string,
89
+ const TTo extends string | undefined = undefined,
90
+ const TMaskFrom extends string = TFrom,
91
+ const TMaskTo extends string = '',
92
+ >(options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>): OctaneAnchorProps;
93
+ function useLinkProps(...args: any[]): Record<string, any> {
94
+ const [user, slot] = splitSlot(args);
95
+ const options = (user[0] ?? {}) as Record<string, any>;
96
+ const router = useRouter();
97
+
98
+ const {
99
+ // custom props
100
+ activeProps,
101
+ inactiveProps,
102
+ activeOptions,
103
+ to,
104
+ preload: userPreload,
105
+ preloadDelay: userPreloadDelay,
106
+ preloadIntentProximity: _preloadIntentProximity,
107
+ hashScrollIntoView,
108
+ replace,
109
+ startTransition,
110
+ resetScroll,
111
+ viewTransition,
112
+ // element props
113
+ children: _children,
114
+ target,
115
+ disabled,
116
+ style,
117
+ class: klass,
118
+ className,
119
+ onClick,
120
+ onBlur,
121
+ onFocus,
122
+ onMouseEnter,
123
+ onMouseLeave,
124
+ onTouchStart,
125
+ ignoreBlocker,
126
+ ref: userRef,
127
+ // consumed by buildLocation — not spread onto the element
128
+ params: _params,
129
+ search: _search,
130
+ hash: _hash,
131
+ state: _state,
132
+ mask: _mask,
133
+ reloadDocument: _reloadDocument,
134
+ unsafeRelative: _unsafeRelative,
135
+ from: _from,
136
+ _fromLocation,
137
+ ...propsSafeToSpread
138
+ } = options;
139
+
140
+ // Subscribe to the location (by href) — active state re-derives per commit.
141
+ const currentLocation: any = useStore(
142
+ router.stores.location,
143
+ (l: any) => l,
144
+ (prev: any, next: any) => prev.href === next.href,
145
+ subSlot(slot, 'lp:loc'),
146
+ );
147
+
148
+ const next = router.buildLocation({ _fromLocation: currentLocation, ...options } as any);
149
+
150
+ const hrefOptionPublicHref = next.maskedLocation
151
+ ? next.maskedLocation.publicHref
152
+ : next.publicHref;
153
+ const hrefOptionExternal = next.maskedLocation ? next.maskedLocation.external : next.external;
154
+ const hrefOption = getHrefOption(
155
+ hrefOptionPublicHref,
156
+ hrefOptionExternal,
157
+ router.history,
158
+ disabled,
159
+ );
160
+
161
+ // External URL detection + dangerous-protocol blocking (javascript:, data:…).
162
+ const externalLink = (() => {
163
+ if (hrefOption?.external) {
164
+ if (isDangerousProtocol(hrefOption.href, router.protocolAllowlist)) return undefined;
165
+ return hrefOption.href;
166
+ }
167
+ if (isSafeInternal(to)) return undefined;
168
+ if (typeof to !== 'string' || to.indexOf(':') === -1) return undefined;
169
+ try {
170
+ new URL(to);
171
+ if (isDangerousProtocol(to, router.protocolAllowlist)) return undefined;
172
+ return to;
173
+ } catch {
174
+ /* not an absolute URL */
175
+ }
176
+ return undefined;
177
+ })();
178
+
179
+ const isActive = (() => {
180
+ if (externalLink) return false;
181
+ if (activeOptions?.exact) {
182
+ if (!exactPathTest(currentLocation.pathname, next.pathname, router.basepath)) {
183
+ return false;
184
+ }
185
+ } else {
186
+ const currentPathSplit = removeTrailingSlash(currentLocation.pathname, router.basepath);
187
+ const nextPathSplit = removeTrailingSlash(next.pathname, router.basepath);
188
+ const pathIsFuzzyEqual =
189
+ currentPathSplit.startsWith(nextPathSplit) &&
190
+ (currentPathSplit.length === nextPathSplit.length ||
191
+ currentPathSplit[nextPathSplit.length] === '/');
192
+ if (!pathIsFuzzyEqual) return false;
193
+ }
194
+ if (activeOptions?.includeSearch ?? true) {
195
+ // router-core 1.171.34 deepEqual: positional (partial, explicitUndefined)
196
+ // instead of an options object; the 4th arg is explicitUndefined directly.
197
+ const searchTest = deepEqual(
198
+ currentLocation.search,
199
+ next.search,
200
+ !activeOptions?.exact,
201
+ activeOptions?.explicitUndefined,
202
+ );
203
+ if (!searchTest) return false;
204
+ }
205
+ if (activeOptions?.includeHash) return currentLocation.hash === next.hash;
206
+ return true;
207
+ })();
208
+
209
+ const resolvedActiveProps: Record<string, any> = isActive
210
+ ? (functionalUpdate(activeProps, {}) ?? STATIC_ACTIVE_OBJECT)
211
+ : STATIC_EMPTY_OBJECT;
212
+ const resolvedInactiveProps: Record<string, any> = isActive
213
+ ? STATIC_EMPTY_OBJECT
214
+ : (functionalUpdate(inactiveProps, {}) ?? STATIC_EMPTY_OBJECT);
215
+
216
+ // Class composes clsx-style (octane normalizeClass folds arrays + falsy).
217
+ const resolvedClass = [
218
+ klass ?? className,
219
+ resolvedActiveProps.class ?? resolvedActiveProps.className,
220
+ resolvedInactiveProps.class ?? resolvedInactiveProps.className,
221
+ ].filter(Boolean);
222
+ const resolvedStyle = mergeStyles(
223
+ style,
224
+ resolvedActiveProps.style,
225
+ resolvedInactiveProps.style,
226
+ );
227
+
228
+ const [isTransitioning, setIsTransitioning] = useState(false, subSlot(slot, 'lp:t'));
229
+ const hasRenderFetched = useRef(false, subSlot(slot, 'lp:rf'));
230
+ const elRef = useRef<Element | null>(null, subSlot(slot, 'lp:el'));
231
+
232
+ const preload =
233
+ options.reloadDocument || externalLink
234
+ ? false
235
+ : (userPreload ?? router.options.defaultPreload);
236
+ const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
237
+
238
+ const doPreload = useCallback(
239
+ () => {
240
+ router.preloadRoute({ ...options, _builtLocation: next } as any).catch((err: unknown) => {
241
+ console.warn(err);
242
+ console.warn(preloadWarning);
243
+ });
244
+ },
245
+ [router, next.href],
246
+ subSlot(slot, 'lp:dp'),
247
+ );
248
+
249
+ // preload="viewport": preload when the anchor scrolls into view (100px margin).
250
+ useEffect(
251
+ () => {
252
+ if (disabled || preload !== 'viewport') return;
253
+ const el = elRef.current;
254
+ if (!el || typeof IntersectionObserver === 'undefined') return;
255
+ const io = new IntersectionObserver(
256
+ (entries) => {
257
+ for (const entry of entries) {
258
+ if (entry.isIntersecting) doPreload();
259
+ }
260
+ },
261
+ { rootMargin: '100px' },
262
+ );
263
+ io.observe(el);
264
+ return () => io.disconnect();
265
+ },
266
+ [disabled, preload, doPreload],
267
+ subSlot(slot, 'lp:io'),
268
+ );
269
+
270
+ // preload="render": preload once on mount.
271
+ useEffect(
272
+ () => {
273
+ if (hasRenderFetched.current) return;
274
+ if (!disabled && preload === 'render') {
275
+ doPreload();
276
+ hasRenderFetched.current = true;
277
+ }
278
+ },
279
+ [disabled, doPreload, preload],
280
+ subSlot(slot, 'lp:pr'),
281
+ );
282
+
283
+ const handleClick = (e: MouseEvent) => {
284
+ const elementTarget = (e.currentTarget as Element | null)?.getAttribute?.('target');
285
+ const effectiveTarget = target !== undefined ? target : elementTarget;
286
+ if (
287
+ !disabled &&
288
+ !isCtrlEvent(e) &&
289
+ !e.defaultPrevented &&
290
+ (!effectiveTarget || effectiveTarget === '_self') &&
291
+ e.button === 0
292
+ ) {
293
+ e.preventDefault();
294
+
295
+ flushSync(() => {
296
+ setIsTransitioning(true);
297
+ });
298
+ const unsub = router.subscribe('onResolved', () => {
299
+ unsub();
300
+ setIsTransitioning(false);
301
+ });
302
+
303
+ router.navigate({
304
+ ...options,
305
+ replace,
306
+ resetScroll,
307
+ hashScrollIntoView,
308
+ startTransition,
309
+ viewTransition,
310
+ ignoreBlocker,
311
+ });
312
+ }
313
+ };
314
+
315
+ const captureRef = (el: Element | null) => {
316
+ elRef.current = el;
317
+ };
318
+ const composedRef = userRef ? [captureRef, userRef] : captureRef;
319
+
320
+ if (externalLink) {
321
+ return {
322
+ ...propsSafeToSpread,
323
+ ref: composedRef,
324
+ href: externalLink,
325
+ ...(target !== undefined && { target }),
326
+ ...(disabled !== undefined && { disabled }),
327
+ ...(resolvedStyle !== undefined && { style: resolvedStyle }),
328
+ ...(resolvedClass.length > 0 && { class: resolvedClass }),
329
+ ...(onClick && { onClick }),
330
+ ...(onBlur && { onBlur }),
331
+ ...(onFocus && { onFocus }),
332
+ ...(onMouseEnter && { onMouseEnter }),
333
+ ...(onMouseLeave && { onMouseLeave }),
334
+ ...(onTouchStart && { onTouchStart }),
335
+ };
336
+ }
337
+
338
+ const enqueueIntentPreload = (e: MouseEvent | FocusEvent) => {
339
+ if (disabled || preload !== 'intent') return;
340
+ if (!preloadDelay) {
341
+ doPreload();
342
+ return;
343
+ }
344
+ const eventTarget = e.currentTarget as EventTarget;
345
+ if (timeoutMap.has(eventTarget)) return;
346
+ const id = setTimeout(() => {
347
+ timeoutMap.delete(eventTarget);
348
+ doPreload();
349
+ }, preloadDelay);
350
+ timeoutMap.set(eventTarget, id);
351
+ };
352
+
353
+ const handleTouchStart = () => {
354
+ if (disabled || preload !== 'intent') return;
355
+ doPreload();
356
+ };
357
+
358
+ const handleLeave = (e: MouseEvent | FocusEvent) => {
359
+ if (disabled || !preload || !preloadDelay) return;
360
+ const eventTarget = e.currentTarget as EventTarget;
361
+ const id = timeoutMap.get(eventTarget);
362
+ if (id) {
363
+ clearTimeout(id);
364
+ timeoutMap.delete(eventTarget);
365
+ }
366
+ };
367
+
368
+ return {
369
+ ...propsSafeToSpread,
370
+ ...resolvedActiveProps,
371
+ ...resolvedInactiveProps,
372
+ href: hrefOption?.href,
373
+ ref: composedRef,
374
+ onClick: composeHandlers([onClick, handleClick]),
375
+ onBlur: composeHandlers([onBlur, handleLeave]),
376
+ onFocus: composeHandlers([onFocus, enqueueIntentPreload]),
377
+ onMouseEnter: composeHandlers([onMouseEnter, enqueueIntentPreload]),
378
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
379
+ onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
380
+ disabled: !!disabled,
381
+ ...(target !== undefined && { target }),
382
+ ...(resolvedStyle !== undefined && { style: resolvedStyle }),
383
+ ...(resolvedClass.length > 0 && { class: resolvedClass }),
384
+ ...(disabled && STATIC_DISABLED_PROPS),
385
+ ...(isActive && STATIC_ACTIVE_PROPS),
386
+ ...(isTransitioning && STATIC_TRANSITIONING_PROPS),
387
+ };
388
+ }
389
+
390
+ return useLinkProps;
391
+ }
@@ -0,0 +1,61 @@
1
+ 'use strong';
2
+
3
+ // A navigating anchor — a thin shell over `useLinkProps` (link.ts), which
4
+ // carries ALL of react-router Link's behavior: href building (mask-aware),
5
+ // external-URL + dangerous-protocol handling, active-state detection,
6
+ // preloading (intent/viewport/render), and the navigate click handler with
7
+ // replace/resetScroll/hashScrollIntoView/viewTransition/startTransition/
8
+ // ignoreBlocker forwarded. Children may be a render prop —
9
+ // `{({ isActive, isTransitioning }) => …}` — and `createLink` renders a custom
10
+ // component via the internal `_asChild` prop. Ref is a prop (octane's model —
11
+ // no forwardRef); `disabled` renders without an href + role="link" (upstream
12
+ // drops the anchor's disabled attribute; the aria props carry the state).
13
+ import { isChildrenBlock } from 'octane';
14
+ import type { useLinkProps as UseLinkProps } from './link.ts';
15
+ import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
16
+ import type { LinkComponentProps, OctaneAnchorProps, UseLinkPropsOptions } from './linkTypes.ts';
17
+
18
+ // Keep the generic implementation signature visible to `.tsrx` consumers so
19
+ // route-aware `to`/`params`/`search` inference and native anchor event types are
20
+ // available before declaration emit.
21
+ export function createLinkComponent(useLinkProps: typeof UseLinkProps) {
22
+ function Link<
23
+ TRouter extends AnyRouter = RegisteredRouter,
24
+ TFrom extends string = string,
25
+ TTo extends string | undefined = undefined,
26
+ TMaskFrom extends string = TFrom,
27
+ TMaskTo extends string = '',
28
+ >(props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) @{
29
+ const {
30
+ _asChild: AsChild,
31
+ children,
32
+ ...rest
33
+ } = props as typeof props & {
34
+ _asChild?: any;
35
+ };
36
+ const linkProps = useLinkProps(
37
+ rest as UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
38
+ ) as OctaneAnchorProps & {
39
+ disabled?: boolean;
40
+ 'data-status'?: string;
41
+ 'data-transitioning'?: string;
42
+ };
43
+ const { disabled: _disabled, ...aProps } = linkProps;
44
+
45
+ const resolvedChildren =
46
+ typeof children === 'function' && !isChildrenBlock(children)
47
+ ? children({
48
+ isActive: linkProps['data-status'] === 'active',
49
+ isTransitioning: linkProps['data-transitioning'] === 'transitioning',
50
+ })
51
+ : children;
52
+
53
+ @if (AsChild) {
54
+ <AsChild {...linkProps}>{resolvedChildren}</AsChild>
55
+ } @else {
56
+ <a {...(aProps as Record<string, any>)}>{resolvedChildren}</a>
57
+ }
58
+ }
59
+
60
+ return Link;
61
+ }
package/src/route.ts CHANGED
@@ -36,6 +36,7 @@ import type {
36
36
  UseNavigateResult,
37
37
  } from '@tanstack/router-core';
38
38
  import { createElement } from 'octane';
39
+ import { createRouteHookBindings } from './routeHookBindings';
39
40
  import type { ComponentBody } from 'octane';
40
41
  import {
41
42
  useMatch,
@@ -107,59 +108,13 @@ declare module '@tanstack/router-core' {
107
108
  // Attach the hook accessors to a route-shaped instance (Route / RootRoute /
108
109
  // RouteApi). `strictLoaderHooks: false` is RouteApi's mode — its loader hooks
109
110
  // pass `strict: false` upstream (the api may be read from ancestor layouts).
110
- function attachRouteHooks(self: any, strictLoaderHooks: boolean): void {
111
- self.useMatch = (...args: any[]) => {
112
- const [user, slot] = splitSlot(args);
113
- const opts = user[0] ?? {};
114
- return useMatch(
115
- { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
116
- subSlot(slot, 'r:m'),
117
- );
118
- };
119
- self.useRouteContext = (...args: any[]) => {
120
- const [user, slot] = splitSlot(args);
121
- const opts = user[0] ?? {};
122
- return useRouteContext({ ...opts, from: self.id }, subSlot(slot, 'r:ctx'));
123
- };
124
- self.useSearch = (...args: any[]) => {
125
- const [user, slot] = splitSlot(args);
126
- const opts = user[0] ?? {};
127
- return useSearch(
128
- { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
129
- subSlot(slot, 'r:s'),
130
- );
131
- };
132
- self.useParams = (...args: any[]) => {
133
- const [user, slot] = splitSlot(args);
134
- const opts = user[0] ?? {};
135
- return useParams(
136
- { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
137
- subSlot(slot, 'r:p'),
138
- );
139
- };
140
- self.useLoaderDeps = (...args: any[]) => {
141
- const [user, slot] = splitSlot(args);
142
- const opts = user[0] ?? {};
143
- return useLoaderDeps(
144
- strictLoaderHooks ? { ...opts, from: self.id } : { ...opts, from: self.id, strict: false },
145
- subSlot(slot, 'r:d'),
146
- );
147
- };
148
- self.useLoaderData = (...args: any[]) => {
149
- const [user, slot] = splitSlot(args);
150
- const opts = user[0] ?? {};
151
- return useLoaderData(
152
- strictLoaderHooks ? { ...opts, from: self.id } : { ...opts, from: self.id, strict: false },
153
- subSlot(slot, 'r:l'),
154
- );
155
- };
156
- }
111
+ const { attachRouteHooks, attachRouteNavigation: attachNavigation } = createRouteHookBindings(
112
+ { useMatch, useParams, useSearch, useLoaderData, useLoaderDeps, useRouteContext, useNavigate },
113
+ useRouter,
114
+ );
157
115
 
158
116
  function attachRouteNavigation(self: any): void {
159
- self.useNavigate = (...args: any[]) => {
160
- const [, slot] = splitSlot(args);
161
- return useNavigate({ from: self.fullPath }, subSlot(slot, 'r:n'));
162
- };
117
+ attachNavigation(self);
163
118
  self.Link = (props: Record<string, unknown>) =>
164
119
  createElement(Link as any, { from: self.fullPath, ...props });
165
120
  }
@@ -0,0 +1,146 @@
1
+ 'use strong';
2
+
3
+ import type * as Hooks from './hooks';
4
+ import type { useRouter as UseRouter } from './context';
5
+ import { splitSlot, subSlot } from './internal';
6
+
7
+ type RouteHooks = Pick<
8
+ typeof Hooks,
9
+ | 'useMatch'
10
+ | 'useParams'
11
+ | 'useSearch'
12
+ | 'useLoaderData'
13
+ | 'useLoaderDeps'
14
+ | 'useRouteContext'
15
+ | 'useNavigate'
16
+ >;
17
+
18
+ // Each entry supplies its context once. Routes retain the same hook slots and
19
+ // inherited router-core combinators without creating ordinary-renderer routes.
20
+ export function createRouteHookBindings(hooks: RouteHooks, useRouter: typeof UseRouter) {
21
+ const {
22
+ useMatch,
23
+ useParams,
24
+ useSearch,
25
+ useLoaderData,
26
+ useLoaderDeps,
27
+ useRouteContext,
28
+ useNavigate,
29
+ } = hooks;
30
+ function attachRouteHooks(self: any, strictLoaderHooks: boolean): void {
31
+ self.useMatch = (...args: any[]) => {
32
+ const [user, slot] = splitSlot(args);
33
+ const opts = user[0] ?? {};
34
+ return useMatch(
35
+ { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
36
+ subSlot(slot, 'r:m'),
37
+ );
38
+ };
39
+ self.useRouteContext = (...args: any[]) => {
40
+ const [user, slot] = splitSlot(args);
41
+ const opts = user[0] ?? {};
42
+ return useRouteContext({ ...opts, from: self.id }, subSlot(slot, 'r:ctx'));
43
+ };
44
+ self.useSearch = (...args: any[]) => {
45
+ const [user, slot] = splitSlot(args);
46
+ const opts = user[0] ?? {};
47
+ return useSearch(
48
+ { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
49
+ subSlot(slot, 'r:s'),
50
+ );
51
+ };
52
+ self.useParams = (...args: any[]) => {
53
+ const [user, slot] = splitSlot(args);
54
+ const opts = user[0] ?? {};
55
+ return useParams(
56
+ { select: opts.select, structuralSharing: opts.structuralSharing, from: self.id },
57
+ subSlot(slot, 'r:p'),
58
+ );
59
+ };
60
+ self.useLoaderDeps = (...args: any[]) => {
61
+ const [user, slot] = splitSlot(args);
62
+ const opts = user[0] ?? {};
63
+ return useLoaderDeps(
64
+ strictLoaderHooks ? { ...opts, from: self.id } : { ...opts, from: self.id, strict: false },
65
+ subSlot(slot, 'r:d'),
66
+ );
67
+ };
68
+ self.useLoaderData = (...args: any[]) => {
69
+ const [user, slot] = splitSlot(args);
70
+ const opts = user[0] ?? {};
71
+ return useLoaderData(
72
+ strictLoaderHooks ? { ...opts, from: self.id } : { ...opts, from: self.id, strict: false },
73
+ subSlot(slot, 'r:l'),
74
+ );
75
+ };
76
+ }
77
+
78
+ function attachRouteNavigation(self: any): void {
79
+ self.useNavigate = (...args: any[]) => {
80
+ const [, slot] = splitSlot(args);
81
+ return useNavigate({ from: self.fullPath }, subSlot(slot, 'r:n'));
82
+ };
83
+ }
84
+
85
+ function attachLazyRouteHooks(self: any) {
86
+ const id = self.options.id;
87
+ self.useMatch = ((...args: Array<any>) => {
88
+ const [user, slot] = splitSlot(args);
89
+ const options = user[0] ?? {};
90
+ return useMatch(
91
+ {
92
+ select: options.select,
93
+ from: id,
94
+ structuralSharing: options.structuralSharing,
95
+ },
96
+ subSlot(slot, 'lr:m'),
97
+ );
98
+ }) as typeof self.useMatch;
99
+ self.useRouteContext = ((...args: Array<any>) => {
100
+ const [user, slot] = splitSlot(args);
101
+ return useRouteContext({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:c'));
102
+ }) as typeof self.useRouteContext;
103
+ self.useSearch = ((...args: Array<any>) => {
104
+ const [user, slot] = splitSlot(args);
105
+ const options = user[0] ?? {};
106
+ return useSearch(
107
+ {
108
+ select: options.select,
109
+ from: id,
110
+ structuralSharing: options.structuralSharing,
111
+ },
112
+ subSlot(slot, 'lr:s'),
113
+ );
114
+ }) as typeof self.useSearch;
115
+ self.useParams = ((...args: Array<any>) => {
116
+ const [user, slot] = splitSlot(args);
117
+ const options = user[0] ?? {};
118
+ return useParams(
119
+ {
120
+ select: options.select,
121
+ from: id,
122
+ structuralSharing: options.structuralSharing,
123
+ },
124
+ subSlot(slot, 'lr:p'),
125
+ );
126
+ }) as typeof self.useParams;
127
+ self.useLoaderDeps = ((...args: Array<any>) => {
128
+ const [user, slot] = splitSlot(args);
129
+ return useLoaderDeps({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:d'));
130
+ }) as typeof self.useLoaderDeps;
131
+ self.useLoaderData = ((...args: Array<any>) => {
132
+ const [user, slot] = splitSlot(args);
133
+ return useLoaderData({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:l'));
134
+ }) as typeof self.useLoaderData;
135
+ self.useNavigate = ((...args: Array<any>) => {
136
+ const [, slot] = splitSlot(args);
137
+ const router = useRouter();
138
+ return useNavigate(
139
+ { from: (router.routesById as Record<string, any>)[id].fullPath },
140
+ subSlot(slot, 'lr:n'),
141
+ );
142
+ }) as typeof self.useNavigate;
143
+ }
144
+
145
+ return { attachRouteHooks, attachRouteNavigation, attachLazyRouteHooks };
146
+ }
@@ -0,0 +1,27 @@
1
+ import { createElement, useId } from 'octane';
2
+ import type { RouterManagedTag } from '@tanstack/router-core';
3
+ import { getAssetKeys } from './assetKeys.ts';
4
+
5
+ // router-core's initial hydration scripts remove themselves before adoption.
6
+ // Host descriptors leave no per-item template frames behind when they do, and
7
+ // create live script elements that execute on later client navigation. This
8
+ // shared owner intentionally remains outside Strong mode for both entries.
9
+ export function createScripts(useScripts: () => Array<RouterManagedTag>) {
10
+ function Scripts() @{
11
+ const owner = useId();
12
+ const scripts = useScripts();
13
+ const keys = getAssetKeys('body', owner, scripts);
14
+ <>
15
+ {scripts.map((script, index) => {
16
+ const { assetKey, managedKey } = keys[index]!;
17
+ return createElement('script', {
18
+ ...script.attrs,
19
+ key: assetKey,
20
+ 'data-tsr-managed-key': managedKey,
21
+ dangerouslySetInnerHTML: { __html: script.children ?? '' },
22
+ });
23
+ })}
24
+ </>
25
+ }
26
+ return Scripts;
27
+ }