@octanejs/tanstack-router 0.1.10 → 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 (58) hide show
  1. package/README.md +24 -11
  2. package/package.json +21 -6
  3. package/src/Asset.tsrx +121 -0
  4. package/src/Asset.tsrx.d.ts +9 -0
  5. package/src/Await.tsrx +2 -1
  6. package/src/Await.tsrx.d.ts +8 -6
  7. package/src/Body.ts +31 -0
  8. package/src/ClientOnly.tsrx +4 -2
  9. package/src/Head.ts +22 -0
  10. package/src/HeadContent.tsrx +41 -0
  11. package/src/HeadContent.tsrx.d.ts +8 -0
  12. package/src/Html.ts +19 -0
  13. package/src/Link.tsrx +19 -7
  14. package/src/Link.tsrx.d.ts +3 -4
  15. package/src/Match.tsrx +40 -13
  16. package/src/Matches.tsrx +7 -7
  17. package/src/Outlet.tsrx +3 -3
  18. package/src/RouteNotFound.tsrx +1 -1
  19. package/src/ScriptOnce.tsrx +23 -0
  20. package/src/ScriptOnce.tsrx.d.ts +3 -0
  21. package/src/Scripts.tsrx +42 -0
  22. package/src/Scripts.tsrx.d.ts +3 -0
  23. package/src/Transitioner.tsrx +15 -13
  24. package/src/assetKeys.ts +11 -0
  25. package/src/context.ts +5 -1
  26. package/src/externalHydration.ts +77 -0
  27. package/src/fileRoute.ts +277 -0
  28. package/src/frameworkTypes.ts +42 -0
  29. package/src/generator-plugin.d.ts +20 -0
  30. package/src/generator-plugin.js +100 -0
  31. package/src/headContentUtils.ts +172 -0
  32. package/src/hooks.ts +151 -0
  33. package/src/index.ts +84 -4
  34. package/src/lazyRouteComponent.ts +2 -1
  35. package/src/link.ts +25 -4
  36. package/src/linkTypes.ts +96 -0
  37. package/src/not-found.tsrx +2 -2
  38. package/src/octane-compiler.d.ts +12 -0
  39. package/src/route.ts +438 -42
  40. package/src/routeHookTypes.ts +228 -0
  41. package/src/router.ts +31 -6
  42. package/src/scriptContentUtils.ts +64 -0
  43. package/src/scroll-restoration.tsrx +16 -0
  44. package/src/scroll-restoration.tsrx.d.ts +3 -0
  45. package/src/ssr/RouterClient.tsrx +36 -0
  46. package/src/ssr/RouterClient.tsrx.d.ts +4 -0
  47. package/src/ssr/RouterServer.tsrx +6 -0
  48. package/src/ssr/RouterServer.tsrx.d.ts +4 -0
  49. package/src/ssr/client.ts +4 -0
  50. package/src/ssr/defaultRenderHandler.ts +11 -0
  51. package/src/ssr/defaultStreamHandler.ts +12 -0
  52. package/src/ssr/renderRouterToStream.ts +195 -0
  53. package/src/ssr/renderRouterToString.ts +58 -0
  54. package/src/ssr/server.ts +8 -0
  55. package/src/structuralSharing.ts +41 -0
  56. package/src/typePrimitives.ts +77 -0
  57. package/src/useAwaited.ts +7 -2
  58. package/src/useRouterState.ts +20 -0
@@ -12,7 +12,7 @@ import { useRouter } from './context.ts';
12
12
 
13
13
  export function RouteNotFound(props: { routeId: string; error?: any }) @{
14
14
  const router = useRouter();
15
- const route = router.routesById[props.routeId];
15
+ const route = (router.routesById as any)[props.routeId];
16
16
  const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
17
17
 
18
18
  @if (NotFound) {
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from 'octane';
2
+ import { isServer } from '@tanstack/router-core/isServer';
3
+ import { useRouter } from './context.ts';
4
+
5
+ export function ScriptOnce(props: { children: string }) @{
6
+ const router = useRouter();
7
+ const server = isServer ?? router.isServer;
8
+ const [hydrating, setHydrating] = useState(true);
9
+ useEffect(() => {
10
+ setHydrating(false);
11
+ }, []);
12
+
13
+ @if (server || hydrating) {
14
+ <script
15
+ nonce={router.options.ssr?.nonce}
16
+ dangerouslySetInnerHTML={{
17
+ __html: props.children,
18
+ }}
19
+ />
20
+ } @else {
21
+ <></>
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { ComponentBody } from 'octane';
2
+
3
+ export declare const ScriptOnce: ComponentBody<{ children: string }>;
@@ -0,0 +1,42 @@
1
+ import { createElement, useEffect, useState } from 'octane';
2
+ import { isServer } from '@tanstack/router-core/isServer';
3
+ import type { RouterManagedTag } from '@tanstack/router-core';
4
+ import { getAssetKey } from './assetKeys.ts';
5
+ import { useRouter } from './context.ts';
6
+ import { useScripts } from './scriptContentUtils.ts';
7
+
8
+ export function Scripts() @{
9
+ const router = useRouter();
10
+ const scripts = useScripts();
11
+ const [hydrating, setHydrating] = useState(true);
12
+ useEffect(() => {
13
+ setHydrating(false);
14
+ }, []);
15
+ const renderScripts: Array<RouterManagedTag> =
16
+ !(isServer ?? router.isServer) && hydrating
17
+ ? [
18
+ {
19
+ tag: 'script',
20
+ attrs: {
21
+ nonce: router.options.ssr?.nonce,
22
+ className: '$tsr',
23
+ id: '$tsr-stream-barrier',
24
+ suppressHydrationWarning: true,
25
+ },
26
+ children: '',
27
+ },
28
+ ...scripts,
29
+ ]
30
+ : scripts;
31
+ <>
32
+ {renderScripts.map((script, index) => {
33
+ const assetKey = getAssetKey('body', script, index);
34
+ return createElement('script', {
35
+ ...script.attrs,
36
+ key: assetKey,
37
+ 'data-tsr-managed-key': assetKey,
38
+ dangerouslySetInnerHTML: { __html: script.children ?? '' },
39
+ });
40
+ })}
41
+ </>
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { ComponentBody } from 'octane';
2
+
3
+ export declare const Scripts: ComponentBody<Record<never, never>>;
@@ -1,4 +1,6 @@
1
- // The navigation engine (renders nothing) — port of react-router's Transitioner.
1
+ // The navigation engine — port of react-router's Transitioner. It runs as a
2
+ // hook because a rendered sibling would put an SSR marker before a root route's
3
+ // document-level <html> node and outside the #__app hydration range.
2
4
  // It (1) supplies `router.startTransition` so every navigation state update rides
3
5
  // an octane transition (concurrent navigation: the current page holds while the
4
6
  // next route suspends), (2) subscribes to history so back/forward and `Link`
@@ -18,7 +20,7 @@ import { useRouter } from './context.ts';
18
20
  import { useStore } from './useStore.ts';
19
21
  import { usePrevious } from './utils.ts';
20
22
 
21
- export function Transitioner() @{
23
+ export function useTransitioner() {
22
24
  const router = useRouter();
23
25
  const mountLoadForRouter = useRef({ router, mounted: false });
24
26
  const [isTransitioning, setIsTransitioning] = useState(false);
@@ -32,7 +34,7 @@ export function Transitioner() @{
32
34
  const isPagePending = isLoading || hasPending;
33
35
  const previousIsPagePending = usePrevious(isPagePending);
34
36
 
35
- router.startTransition = (fn) => {
37
+ router.startTransition = (fn: () => void) => {
36
38
  setIsTransitioning(true);
37
39
  startTransition(() => {
38
40
  fn();
@@ -47,14 +49,16 @@ export function Transitioner() @{
47
49
  useEffect(() => {
48
50
  const unsub = router.history.subscribe(() => router.load());
49
51
 
50
- const nextLocation = router.buildLocation({
51
- to: router.latestLocation.pathname,
52
- search: true,
53
- params: true,
54
- hash: true,
55
- state: true,
56
- _includeValidateSearch: true,
57
- });
52
+ const nextLocation = router.buildLocation(
53
+ {
54
+ to: router.latestLocation.pathname,
55
+ search: true,
56
+ params: true,
57
+ hash: true,
58
+ state: true,
59
+ _includeValidateSearch: true,
60
+ } as any,
61
+ );
58
62
  if (
59
63
  trimPathRight(router.latestLocation.publicHref) !== trimPathRight(nextLocation.publicHref)
60
64
  ) {
@@ -121,6 +125,4 @@ export function Transitioner() @{
121
125
  });
122
126
  }
123
127
  }, [isAnyPending, previousIsAnyPending, router]);
124
-
125
- <></>
126
128
  }
@@ -0,0 +1,11 @@
1
+ import type { RouterManagedTag } from '@tanstack/router-core';
2
+
3
+ export function getAssetKey(scope: 'head' | 'body', asset: RouterManagedTag, index: number) {
4
+ const inlineCss = asset.tag === 'style' && asset.inlineCss;
5
+ return `${scope}:${index}:${JSON.stringify({
6
+ tag: asset.tag,
7
+ attrs: asset.attrs,
8
+ children: inlineCss ? undefined : asset.children,
9
+ inlineCss,
10
+ })}`;
11
+ }
package/src/context.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // match id down the render tree so `Outlet` can find the NEXT match to render —
4
4
  // the pull-based chaining that replaces a top-down state diff.
5
5
  import { createContext, useContext } from 'octane';
6
- import type { AnyRouter } from '@tanstack/router-core';
6
+ import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
7
7
 
8
8
  export const routerContext = createContext<AnyRouter | undefined>(undefined);
9
9
  export const getRouterContext = (): typeof routerContext => routerContext;
@@ -14,6 +14,10 @@ export const matchContext = createContext<string | undefined>(undefined);
14
14
  // Resolve the active router: an explicitly-passed one wins, else the context.
15
15
  // `useContext` is keyed by context identity (not a per-call-site slot), so it's
16
16
  // safe to call from this binding code without a slot.
17
+ export function useRouter<TRouter extends AnyRouter = RegisteredRouter>(opts?: {
18
+ router?: TRouter;
19
+ warn?: boolean;
20
+ }): TRouter;
17
21
  export function useRouter(...args: unknown[]): AnyRouter {
18
22
  const opts = (args.length && typeof args[0] !== 'symbol' ? args[0] : undefined) as
19
23
  | { router?: AnyRouter; warn?: boolean }
@@ -0,0 +1,77 @@
1
+ const EXTERNAL_HYDRATION_PROMISE = Symbol.for('octane.external-hydration-promise');
2
+
3
+ type ExternalHydrationThenable<T> = PromiseLike<T> & {
4
+ [EXTERNAL_HYDRATION_PROMISE]: true;
5
+ status?: ThenableStatus;
6
+ value?: T;
7
+ reason?: unknown;
8
+ };
9
+
10
+ type ThenableStatus = 'pending' | 'fulfilled' | 'rejected';
11
+
12
+ const externalHydrationThenables = new WeakMap<object, ExternalHydrationThenable<unknown>>();
13
+
14
+ /**
15
+ * Wrap a router-owned promise so Octane still schedules its suspense boundary,
16
+ * while TanStack's serializer remains the only owner of its hydration value.
17
+ */
18
+ export function toExternalHydrationThenable<T>(thenable: PromiseLike<T>): PromiseLike<T> {
19
+ const key = thenable as object;
20
+ const existing = externalHydrationThenables.get(key);
21
+ if (existing) {
22
+ return existing as ExternalHydrationThenable<T>;
23
+ }
24
+
25
+ let localStatus: ThenableStatus | undefined;
26
+ let localValue: T | undefined;
27
+ let localReason: unknown;
28
+ let hasLocalValue = false;
29
+ let hasLocalReason = false;
30
+
31
+ const externalThenable: ExternalHydrationThenable<T> = {
32
+ [EXTERNAL_HYDRATION_PROMISE]: true,
33
+ get status() {
34
+ return localStatus ?? readThenableStatus(thenable);
35
+ },
36
+ set status(status) {
37
+ localStatus = status;
38
+ },
39
+ get value(): T | undefined {
40
+ return hasLocalValue ? localValue : readThenableProperty<T>(thenable, 'value');
41
+ },
42
+ set value(value: T | undefined) {
43
+ hasLocalValue = true;
44
+ localValue = value;
45
+ },
46
+ get reason(): unknown {
47
+ return hasLocalReason ? localReason : readThenableProperty<unknown>(thenable, 'reason');
48
+ },
49
+ set reason(reason) {
50
+ hasLocalReason = true;
51
+ localReason = reason;
52
+ },
53
+ then(onfulfilled, onrejected) {
54
+ return thenable.then(onfulfilled, onrejected);
55
+ },
56
+ };
57
+ externalHydrationThenables.set(key, externalThenable);
58
+ return externalThenable;
59
+ }
60
+
61
+ function readThenableStatus(thenable: PromiseLike<unknown>) {
62
+ const status = readThenableProperty(thenable, 'status');
63
+ return status === 'pending' || status === 'fulfilled' || status === 'rejected'
64
+ ? status
65
+ : undefined;
66
+ }
67
+
68
+ function readThenableProperty<T>(
69
+ thenable: PromiseLike<unknown>,
70
+ property: 'status' | 'value' | 'reason',
71
+ ): T | undefined {
72
+ try {
73
+ return (thenable as PromiseLike<unknown> & Record<string, T>)[property];
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
@@ -0,0 +1,277 @@
1
+ import { createRoute } from './route';
2
+ import {
3
+ useLoaderData,
4
+ useLoaderDeps,
5
+ useMatch,
6
+ useNavigate,
7
+ useParams,
8
+ useRouteContext,
9
+ useSearch,
10
+ } from './hooks';
11
+ import { useRouter } from './context';
12
+ import { splitSlot, subSlot } from './internal';
13
+ import type {
14
+ AnyContext,
15
+ AnyRoute,
16
+ AnyRouter,
17
+ Constrain,
18
+ ConstrainLiteral,
19
+ FileBaseRouteOptions,
20
+ FileRoutesByPath,
21
+ LazyRouteOptions,
22
+ Register,
23
+ RegisteredRouter,
24
+ ResolveParams,
25
+ Route,
26
+ RouteById,
27
+ RouteConstraints,
28
+ RouteIds,
29
+ RouteLoaderEntry,
30
+ UpdatableRouteOptions,
31
+ UseNavigateResult,
32
+ } from '@tanstack/router-core';
33
+ import type {
34
+ UseLoaderDataRoute,
35
+ UseLoaderDepsRoute,
36
+ UseMatchRoute,
37
+ UseParamsRoute,
38
+ UseRouteContextRoute,
39
+ UseSearchRoute,
40
+ } from './routeHookTypes';
41
+
42
+ declare const process: {
43
+ env: {
44
+ NODE_ENV?: string;
45
+ };
46
+ };
47
+
48
+ export function createFileRoute<
49
+ TFilePath extends keyof FileRoutesByPath,
50
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
51
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
52
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
53
+ TFullPath extends RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
54
+ >(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>['createRoute'] {
55
+ return new FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>(path, {
56
+ silent: true,
57
+ }).createRoute;
58
+ }
59
+
60
+ /** @deprecated Use `createFileRoute(path)(options)` instead. */
61
+ export class FileRoute<
62
+ TFilePath extends keyof FileRoutesByPath,
63
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
64
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
65
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
66
+ TFullPath extends RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
67
+ > {
68
+ silent?: boolean;
69
+
70
+ constructor(
71
+ public path?: TFilePath,
72
+ _opts?: { silent: boolean },
73
+ ) {
74
+ this.silent = _opts?.silent;
75
+ }
76
+
77
+ createRoute = <
78
+ TRegister = Register,
79
+ TSearchValidator = undefined,
80
+ TParams = ResolveParams<TPath>,
81
+ TRouteContextFn = AnyContext,
82
+ TBeforeLoadFn = AnyContext,
83
+ TLoaderDeps extends Record<string, any> = {},
84
+ TLoaderFn = undefined,
85
+ TChildren = unknown,
86
+ TSSR = unknown,
87
+ const TMiddlewares = unknown,
88
+ THandlers = undefined,
89
+ >(
90
+ options?: FileBaseRouteOptions<
91
+ TRegister,
92
+ TParentRoute,
93
+ TId,
94
+ TPath,
95
+ TSearchValidator,
96
+ TParams,
97
+ TLoaderDeps,
98
+ TLoaderFn,
99
+ AnyContext,
100
+ TRouteContextFn,
101
+ TBeforeLoadFn,
102
+ AnyContext,
103
+ TSSR,
104
+ TMiddlewares,
105
+ THandlers
106
+ > &
107
+ UpdatableRouteOptions<
108
+ TParentRoute,
109
+ TId,
110
+ TFullPath,
111
+ TParams,
112
+ TSearchValidator,
113
+ TLoaderFn,
114
+ TLoaderDeps,
115
+ AnyContext,
116
+ TRouteContextFn,
117
+ TBeforeLoadFn
118
+ >,
119
+ ): Route<
120
+ TRegister,
121
+ TParentRoute,
122
+ TPath,
123
+ TFullPath,
124
+ TFilePath,
125
+ TId,
126
+ TSearchValidator,
127
+ TParams,
128
+ AnyContext,
129
+ TRouteContextFn,
130
+ TBeforeLoadFn,
131
+ TLoaderDeps,
132
+ TLoaderFn,
133
+ TChildren,
134
+ unknown,
135
+ TSSR,
136
+ TMiddlewares,
137
+ THandlers
138
+ > => {
139
+ if (process.env.NODE_ENV !== 'production' && !this.silent) {
140
+ console.warn('Warning: FileRoute is deprecated. Use createFileRoute(path)(options) instead.');
141
+ }
142
+ const route = createRoute(options as any);
143
+ (route as any).isRoot = false;
144
+ return route as any;
145
+ };
146
+ }
147
+
148
+ /** @deprecated Place the loader in the main `createFileRoute` options. */
149
+ export function FileRouteLoader<
150
+ TFilePath extends keyof FileRoutesByPath,
151
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
152
+ >(
153
+ _path: TFilePath,
154
+ ): <TLoaderFn>(
155
+ loaderFn: Constrain<
156
+ TLoaderFn,
157
+ RouteLoaderEntry<
158
+ Register,
159
+ TRoute['parentRoute'],
160
+ TRoute['types']['id'],
161
+ TRoute['types']['params'],
162
+ TRoute['types']['loaderDeps'],
163
+ TRoute['types']['routerContext'],
164
+ TRoute['types']['routeContextFn'],
165
+ TRoute['types']['beforeLoadFn']
166
+ >
167
+ >,
168
+ ) => TLoaderFn {
169
+ if (process.env.NODE_ENV !== 'production') {
170
+ console.warn(
171
+ 'Warning: FileRouteLoader is deprecated. Place the loader in createFileRoute options.',
172
+ );
173
+ }
174
+ return (loaderFn) => loaderFn as never;
175
+ }
176
+
177
+ declare module '@tanstack/router-core' {
178
+ export interface LazyRoute<in out TRoute extends AnyRoute> {
179
+ useMatch: UseMatchRoute<TRoute['id']>;
180
+ useRouteContext: UseRouteContextRoute<TRoute['id']>;
181
+ useSearch: UseSearchRoute<TRoute['id']>;
182
+ useParams: UseParamsRoute<TRoute['id']>;
183
+ useLoaderDeps: UseLoaderDepsRoute<TRoute['id']>;
184
+ useLoaderData: UseLoaderDataRoute<TRoute['id']>;
185
+ useNavigate: () => UseNavigateResult<TRoute['fullPath']>;
186
+ }
187
+ }
188
+
189
+ export class LazyRoute<TRoute extends AnyRoute> {
190
+ options: { id: string } & LazyRouteOptions;
191
+ declare useMatch: UseMatchRoute<TRoute['id']>;
192
+ declare useRouteContext: UseRouteContextRoute<TRoute['id']>;
193
+ declare useSearch: UseSearchRoute<TRoute['id']>;
194
+ declare useParams: UseParamsRoute<TRoute['id']>;
195
+ declare useLoaderDeps: UseLoaderDepsRoute<TRoute['id']>;
196
+ declare useLoaderData: UseLoaderDataRoute<TRoute['id']>;
197
+ declare useNavigate: () => UseNavigateResult<TRoute['fullPath']>;
198
+
199
+ constructor(opts: { id: string } & LazyRouteOptions) {
200
+ this.options = opts;
201
+ const id = this.options.id;
202
+ this.useMatch = ((...args: Array<any>) => {
203
+ const [user, slot] = splitSlot(args);
204
+ const options = user[0] ?? {};
205
+ return useMatch(
206
+ {
207
+ select: options.select,
208
+ from: id,
209
+ structuralSharing: options.structuralSharing,
210
+ },
211
+ subSlot(slot, 'lr:m'),
212
+ );
213
+ }) as typeof this.useMatch;
214
+ this.useRouteContext = ((...args: Array<any>) => {
215
+ const [user, slot] = splitSlot(args);
216
+ return useRouteContext({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:c'));
217
+ }) as typeof this.useRouteContext;
218
+ this.useSearch = ((...args: Array<any>) => {
219
+ const [user, slot] = splitSlot(args);
220
+ const options = user[0] ?? {};
221
+ return useSearch(
222
+ {
223
+ select: options.select,
224
+ from: id,
225
+ structuralSharing: options.structuralSharing,
226
+ },
227
+ subSlot(slot, 'lr:s'),
228
+ );
229
+ }) as typeof this.useSearch;
230
+ this.useParams = ((...args: Array<any>) => {
231
+ const [user, slot] = splitSlot(args);
232
+ const options = user[0] ?? {};
233
+ return useParams(
234
+ {
235
+ select: options.select,
236
+ from: id,
237
+ structuralSharing: options.structuralSharing,
238
+ },
239
+ subSlot(slot, 'lr:p'),
240
+ );
241
+ }) as typeof this.useParams;
242
+ this.useLoaderDeps = ((...args: Array<any>) => {
243
+ const [user, slot] = splitSlot(args);
244
+ return useLoaderDeps({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:d'));
245
+ }) as typeof this.useLoaderDeps;
246
+ this.useLoaderData = ((...args: Array<any>) => {
247
+ const [user, slot] = splitSlot(args);
248
+ return useLoaderData({ ...(user[0] ?? {}), from: id }, subSlot(slot, 'lr:l'));
249
+ }) as typeof this.useLoaderData;
250
+ this.useNavigate = ((...args: Array<any>) => {
251
+ const [, slot] = splitSlot(args);
252
+ const router = useRouter();
253
+ return useNavigate(
254
+ { from: (router.routesById as Record<string, any>)[id].fullPath },
255
+ subSlot(slot, 'lr:n'),
256
+ );
257
+ }) as typeof this.useNavigate;
258
+ }
259
+ }
260
+
261
+ export function createLazyRoute<
262
+ TRouter extends AnyRouter = RegisteredRouter,
263
+ TId extends string = string,
264
+ TRoute extends AnyRoute = RouteById<TRouter['routeTree'], TId>,
265
+ >(id: ConstrainLiteral<TId, RouteIds<TRouter['routeTree']>>) {
266
+ return (opts: LazyRouteOptions) => new LazyRoute<TRoute>({ id, ...opts });
267
+ }
268
+
269
+ export function createLazyFileRoute<
270
+ TFilePath extends keyof FileRoutesByPath,
271
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
272
+ >(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute<TRoute> {
273
+ if (typeof id === 'object') {
274
+ return new LazyRoute<TRoute>(id) as any;
275
+ }
276
+ return (opts: LazyRouteOptions) => new LazyRoute<TRoute>({ id, ...opts });
277
+ }
@@ -0,0 +1,42 @@
1
+ import type { MetaDescriptor, UseNavigateResult } from '@tanstack/router-core';
2
+ import type { LinkComponentRoute } from './linkTypes';
3
+ import type {
4
+ UseLoaderDataRoute,
5
+ UseLoaderDepsRoute,
6
+ UseMatchRoute,
7
+ UseParamsRoute,
8
+ UseRouteContextRoute,
9
+ UseSearchRoute,
10
+ } from './routeHookTypes';
11
+
12
+ export type OctaneElementAttributes = Record<string, string | number | boolean | null | undefined>;
13
+
14
+ export type OctaneScriptAttributes = OctaneElementAttributes & {
15
+ children?: string;
16
+ };
17
+
18
+ declare module '@tanstack/router-core' {
19
+ interface RouteMatchExtensions {
20
+ // router-core 1.171.15's source RouteMatch carries this field, and its SSR
21
+ // declarations index it while the published Matches.d.ts accidentally omits
22
+ // it. Keep the binding's public SSR entry type-checkable without asking
23
+ // consumers to enable skipLibCheck.
24
+ __beforeLoadContext?: Record<string, unknown>;
25
+ meta?: Array<MetaDescriptor | undefined>;
26
+ links?: Array<OctaneElementAttributes | undefined>;
27
+ scripts?: Array<OctaneScriptAttributes | undefined>;
28
+ styles?: Array<OctaneScriptAttributes | undefined>;
29
+ headScripts?: Array<OctaneScriptAttributes | undefined>;
30
+ }
31
+
32
+ interface RouteExtensions<in out TId extends string, in out TFullPath extends string> {
33
+ useMatch: UseMatchRoute<TId>;
34
+ useRouteContext: UseRouteContextRoute<TId>;
35
+ useSearch: UseSearchRoute<TId>;
36
+ useParams: UseParamsRoute<TId>;
37
+ useLoaderDeps: UseLoaderDepsRoute<TId>;
38
+ useLoaderData: UseLoaderDataRoute<TId>;
39
+ useNavigate: () => UseNavigateResult<TFullPath>;
40
+ Link: LinkComponentRoute<TFullPath>;
41
+ }
42
+ }
@@ -0,0 +1,20 @@
1
+ export interface TransformRouteSourceOptions {
2
+ source: string;
3
+ filename: string;
4
+ node: unknown;
5
+ }
6
+
7
+ export interface FormatRouteOptions {
8
+ source: string;
9
+ node: unknown;
10
+ }
11
+
12
+ export interface OctaneRouteGeneratorPlugin {
13
+ name: string;
14
+ transformRouteSource: (options: TransformRouteSourceOptions) => string;
15
+ formatRoute: (options: FormatRouteOptions) => string;
16
+ }
17
+
18
+ export declare function maskOctaneRouteSource(source: string, filename?: string): string;
19
+
20
+ export declare function octaneRouteGeneratorPlugin(): OctaneRouteGeneratorPlugin;
@@ -0,0 +1,100 @@
1
+ import { compileToVolarMappings } from 'octane/compiler/volar';
2
+
3
+ /**
4
+ * @typedef {object} AstNode
5
+ * @property {AstNode | Array<AstNode>} [body]
6
+ * @property {number} [start]
7
+ * @property {number} [end]
8
+ * @property {{ native_tsrx_body?: boolean }} [metadata]
9
+ */
10
+
11
+ /**
12
+ * Makes TSRX route modules parseable by the router generator without changing
13
+ * source offsets. The generator applies edits to the original source, so the
14
+ * authored Octane template bodies remain byte-for-byte intact.
15
+ *
16
+ * @param {string} source
17
+ * @param {string} [filename]
18
+ * @returns {string}
19
+ */
20
+ export function maskOctaneRouteSource(source, filename = 'route.tsrx') {
21
+ const { sourceAst } = compileToVolarMappings(source, filename);
22
+ const output = source.split('');
23
+
24
+ for (const body of findNativeTemplateBodies(/** @type {AstNode} */ (sourceAst))) {
25
+ const { start, end } = body;
26
+ output[start] = ' ';
27
+ output[start + 1] = '{';
28
+ for (let index = start + 2; index < end - 1; index++) {
29
+ if (source[index] !== '\n' && source[index] !== '\r') {
30
+ output[index] = ' ';
31
+ }
32
+ }
33
+ output[end - 1] = '}';
34
+ }
35
+
36
+ return output.join('');
37
+ }
38
+
39
+ /**
40
+ * @returns {{
41
+ * name: string
42
+ * transformRouteSource: (options: { source: string, filename: string }) => string
43
+ * formatRoute: (options: { source: string }) => string
44
+ * }}
45
+ */
46
+ export function octaneRouteGeneratorPlugin() {
47
+ return {
48
+ name: 'octane-route-source',
49
+ transformRouteSource: ({ source, filename }) => maskOctaneRouteSource(source, filename),
50
+ // Router scaffolds are already formatted. Returning them unchanged avoids
51
+ // passing TSRX's `@{}` syntax through a TypeScript-only formatter.
52
+ formatRoute: ({ source }) => source,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {AstNode} root
58
+ * @returns {Array<{ start: number, end: number }>}
59
+ */
60
+ function findNativeTemplateBodies(root) {
61
+ /** @type {Array<{ start: number, end: number }>} */
62
+ const bodies = [];
63
+ const visited = new WeakSet();
64
+
65
+ /** @param {unknown} value */
66
+ const visit = (value) => {
67
+ if (!value || typeof value !== 'object' || visited.has(value)) {
68
+ return;
69
+ }
70
+ visited.add(value);
71
+
72
+ if (Array.isArray(value)) {
73
+ for (const item of value) {
74
+ visit(item);
75
+ }
76
+ return;
77
+ }
78
+
79
+ const node = /** @type {AstNode} */ (value);
80
+ if (
81
+ node.metadata?.native_tsrx_body === true &&
82
+ node.body &&
83
+ !Array.isArray(node.body) &&
84
+ typeof node.body.start === 'number' &&
85
+ typeof node.body.end === 'number'
86
+ ) {
87
+ bodies.push({ start: node.body.start, end: node.body.end });
88
+ return;
89
+ }
90
+
91
+ for (const [key, child] of Object.entries(node)) {
92
+ if (key !== 'metadata' && key !== 'loc') {
93
+ visit(child);
94
+ }
95
+ }
96
+ };
97
+
98
+ visit(root);
99
+ return bodies;
100
+ }