@octanejs/tanstack-router 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +24 -11
  2. package/package.json +22 -7
  3. package/src/Asset.tsrx +121 -0
  4. package/src/Asset.tsrx.d.ts +9 -0
  5. package/src/Await.tsrx +9 -3
  6. package/src/Await.tsrx.d.ts +8 -6
  7. package/src/Body.ts +31 -0
  8. package/src/CatchBoundary.tsrx +21 -4
  9. package/src/CatchBoundary.tsrx.d.ts +10 -4
  10. package/src/ClientOnly.tsrx +6 -3
  11. package/src/Head.ts +22 -0
  12. package/src/HeadContent.tsrx +41 -0
  13. package/src/HeadContent.tsrx.d.ts +8 -0
  14. package/src/Html.ts +19 -0
  15. package/src/Link.tsrx +19 -4
  16. package/src/Link.tsrx.d.ts +3 -4
  17. package/src/Match.tsrx +61 -29
  18. package/src/MatchRoute.tsrx +6 -3
  19. package/src/Matches.tsrx +7 -7
  20. package/src/Navigate.tsrx +2 -1
  21. package/src/Outlet.tsrx +8 -6
  22. package/src/RouteNotFound.tsrx +2 -2
  23. package/src/RouterProvider.tsrx +12 -2
  24. package/src/RouterProvider.tsrx.d.ts +9 -5
  25. package/src/SafeFragment.tsrx +4 -1
  26. package/src/ScriptOnce.tsrx +23 -0
  27. package/src/ScriptOnce.tsrx.d.ts +3 -0
  28. package/src/Scripts.tsrx +42 -0
  29. package/src/Scripts.tsrx.d.ts +3 -0
  30. package/src/Transitioner.tsrx +15 -13
  31. package/src/assetKeys.ts +11 -0
  32. package/src/context.ts +5 -1
  33. package/src/externalHydration.ts +77 -0
  34. package/src/fileRoute.ts +277 -0
  35. package/src/frameworkTypes.ts +42 -0
  36. package/src/generator-plugin.d.ts +20 -0
  37. package/src/generator-plugin.js +100 -0
  38. package/src/headContentUtils.ts +172 -0
  39. package/src/hooks.ts +151 -0
  40. package/src/index.ts +95 -3
  41. package/src/lazyRouteComponent.ts +2 -1
  42. package/src/link.ts +25 -4
  43. package/src/linkTypes.ts +96 -0
  44. package/src/not-found.tsrx +19 -6
  45. package/src/not-found.tsrx.d.ts +5 -2
  46. package/src/octane-compiler.d.ts +12 -0
  47. package/src/route.ts +473 -36
  48. package/src/routeHookTypes.ts +228 -0
  49. package/src/router.ts +31 -6
  50. package/src/scriptContentUtils.ts +64 -0
  51. package/src/scroll-restoration.tsrx +16 -0
  52. package/src/scroll-restoration.tsrx.d.ts +3 -0
  53. package/src/ssr/RouterClient.tsrx +36 -0
  54. package/src/ssr/RouterClient.tsrx.d.ts +4 -0
  55. package/src/ssr/RouterServer.tsrx +6 -0
  56. package/src/ssr/RouterServer.tsrx.d.ts +4 -0
  57. package/src/ssr/client.ts +4 -0
  58. package/src/ssr/defaultRenderHandler.ts +11 -0
  59. package/src/ssr/defaultStreamHandler.ts +12 -0
  60. package/src/ssr/renderRouterToStream.ts +195 -0
  61. package/src/ssr/renderRouterToString.ts +58 -0
  62. package/src/ssr/server.ts +8 -0
  63. package/src/structuralSharing.ts +41 -0
  64. package/src/typePrimitives.ts +77 -0
  65. package/src/useAwaited.ts +7 -2
  66. package/src/useBlocker.tsrx +73 -8
  67. package/src/useBlocker.tsrx.d.ts +58 -2
  68. package/src/useRouterState.ts +20 -0
  69. package/src/useStore.ts +17 -6
@@ -0,0 +1,58 @@
1
+ import { renderToString as octaneRenderToString } from 'octane/server';
2
+ import type { ComponentBody } from 'octane';
3
+ import type { AnyRouter } from '@tanstack/router-core';
4
+
5
+ type RouterApp = ComponentBody<{ router: AnyRouter }>;
6
+ type ServerComponent = Parameters<typeof octaneRenderToString>[0];
7
+
8
+ // eslint-disable-next-line @typescript-eslint/require-await -- framework render handlers share an async contract
9
+ export async function renderRouterToString({
10
+ router,
11
+ responseHeaders,
12
+ App,
13
+ }: {
14
+ router: AnyRouter;
15
+ responseHeaders: Headers;
16
+ App: RouterApp;
17
+ }) {
18
+ try {
19
+ const result = octaneRenderToString(
20
+ App as unknown as ServerComponent,
21
+ { router },
22
+ { nonce: router.options.ssr?.nonce },
23
+ );
24
+ router.serverSsr!.setRenderFinished();
25
+
26
+ return new Response(
27
+ finalizeBufferedHtml(result.html, result.css, router.serverSsr!.takeBufferedHtml()),
28
+ {
29
+ status: router.stores.statusCode.get(),
30
+ headers: responseHeaders,
31
+ },
32
+ );
33
+ } catch (error) {
34
+ console.error('Render to string error:', error);
35
+ return new Response('Internal Server Error', {
36
+ status: 500,
37
+ headers: responseHeaders,
38
+ });
39
+ } finally {
40
+ router.serverSsr?.cleanup();
41
+ }
42
+ }
43
+
44
+ export function finalizeBufferedHtml(renderedHtml: string, css: string, injectedHtml?: string) {
45
+ let html = renderedHtml;
46
+
47
+ if (css) {
48
+ html = html.includes('</head>') ? html.replace('</head>', `${css}</head>`) : `${css}${html}`;
49
+ }
50
+
51
+ if (injectedHtml) {
52
+ html = html.includes('</body>')
53
+ ? html.replace('</body>', `${injectedHtml}</body>`)
54
+ : `${html}${injectedHtml}`;
55
+ }
56
+
57
+ return `<!DOCTYPE html>${html}`;
58
+ }
@@ -0,0 +1,8 @@
1
+ import '../frameworkTypes';
2
+
3
+ export { RouterServer } from './RouterServer.tsrx';
4
+ export { defaultRenderHandler } from './defaultRenderHandler';
5
+ export { defaultStreamHandler } from './defaultStreamHandler';
6
+ export { renderRouterToStream } from './renderRouterToStream';
7
+ export { renderRouterToString } from './renderRouterToString';
8
+ export * from '@tanstack/router-core/ssr/server';
@@ -0,0 +1,41 @@
1
+ import type {
2
+ AnyRouter,
3
+ Constrain,
4
+ OptionalStructuralSharing,
5
+ ValidateJSON,
6
+ } from '@tanstack/router-core';
7
+
8
+ export type DefaultStructuralSharingEnabled<TRouter extends AnyRouter> =
9
+ boolean extends TRouter['options']['defaultStructuralSharing']
10
+ ? false
11
+ : NonNullable<TRouter['options']['defaultStructuralSharing']>;
12
+
13
+ export interface RequiredStructuralSharing<TStructuralSharing, TConstraint> {
14
+ readonly structuralSharing: Constrain<TStructuralSharing, TConstraint>;
15
+ }
16
+
17
+ export type StructuralSharingOption<
18
+ TRouter extends AnyRouter,
19
+ TSelected,
20
+ TStructuralSharing,
21
+ > = unknown extends TSelected
22
+ ? OptionalStructuralSharing<TStructuralSharing, boolean>
23
+ : unknown extends TRouter['routeTree']
24
+ ? OptionalStructuralSharing<TStructuralSharing, boolean>
25
+ : TSelected extends ValidateJSON<TSelected>
26
+ ? OptionalStructuralSharing<TStructuralSharing, boolean>
27
+ : DefaultStructuralSharingEnabled<TRouter> extends true
28
+ ? RequiredStructuralSharing<TStructuralSharing, false>
29
+ : OptionalStructuralSharing<TStructuralSharing, false>;
30
+
31
+ export type StructuralSharingEnabled<
32
+ TRouter extends AnyRouter,
33
+ TStructuralSharing,
34
+ > = boolean extends TStructuralSharing
35
+ ? DefaultStructuralSharingEnabled<TRouter>
36
+ : TStructuralSharing;
37
+
38
+ export type ValidateSelected<TRouter extends AnyRouter, TSelected, TStructuralSharing> =
39
+ StructuralSharingEnabled<TRouter, TStructuralSharing> extends true
40
+ ? ValidateJSON<TSelected>
41
+ : TSelected;
@@ -0,0 +1,77 @@
1
+ import type {
2
+ AnyRouter,
3
+ Constrain,
4
+ InferFrom,
5
+ InferMaskFrom,
6
+ InferMaskTo,
7
+ InferSelected,
8
+ InferShouldThrow,
9
+ InferStrict,
10
+ InferTo,
11
+ RegisteredRouter,
12
+ } from '@tanstack/router-core';
13
+ import type { LinkComponentProps } from './linkTypes';
14
+ import type { UseParamsOptions, UseSearchOptions } from './routeHookTypes';
15
+
16
+ export type ValidateLinkOptions<
17
+ TRouter extends AnyRouter = RegisteredRouter,
18
+ TOptions = unknown,
19
+ TDefaultFrom extends string = string,
20
+ TComp = 'a',
21
+ > = Constrain<
22
+ TOptions,
23
+ LinkComponentProps<
24
+ TComp,
25
+ TRouter,
26
+ InferFrom<TOptions, TDefaultFrom>,
27
+ InferTo<TOptions>,
28
+ InferMaskFrom<TOptions>,
29
+ InferMaskTo<TOptions>
30
+ >
31
+ >;
32
+
33
+ /** @private */
34
+ export type InferStructuralSharing<TOptions> = TOptions extends {
35
+ structuralSharing: infer TStructuralSharing;
36
+ }
37
+ ? TStructuralSharing
38
+ : unknown;
39
+
40
+ export type ValidateUseSearchOptions<
41
+ TOptions,
42
+ TRouter extends AnyRouter = RegisteredRouter,
43
+ > = Constrain<
44
+ TOptions,
45
+ UseSearchOptions<
46
+ TRouter,
47
+ InferFrom<TOptions>,
48
+ InferStrict<TOptions>,
49
+ InferShouldThrow<TOptions>,
50
+ InferSelected<TOptions>,
51
+ InferStructuralSharing<TOptions>
52
+ >
53
+ >;
54
+
55
+ export type ValidateUseParamsOptions<
56
+ TOptions,
57
+ TRouter extends AnyRouter = RegisteredRouter,
58
+ > = Constrain<
59
+ TOptions,
60
+ UseParamsOptions<
61
+ TRouter,
62
+ InferFrom<TOptions>,
63
+ InferStrict<TOptions>,
64
+ InferShouldThrow<TOptions>,
65
+ InferSelected<TOptions>,
66
+ InferStructuralSharing<TOptions>
67
+ >
68
+ >;
69
+
70
+ export type ValidateLinkOptionsArray<
71
+ TRouter extends AnyRouter = RegisteredRouter,
72
+ TOptions extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
73
+ TDefaultFrom extends string = string,
74
+ TComp = 'a',
75
+ > = {
76
+ [K in keyof TOptions]: ValidateLinkOptions<TRouter, TOptions[K], TDefaultFrom, TComp>;
77
+ };
package/src/useAwaited.ts CHANGED
@@ -3,7 +3,12 @@
3
3
  // this is a one-liner. The promise is typically produced by router-core's `defer()`
4
4
  // in a loader and streamed to the client.
5
5
  import { use } from 'octane';
6
+ import { toExternalHydrationThenable } from './externalHydration';
6
7
 
7
- export function useAwaited(opts: { promise: any }): any {
8
- return use(opts.promise);
8
+ export type AwaitOptions<T> = {
9
+ promise: Promise<T>;
10
+ };
11
+
12
+ export function useAwaited<T>(opts: AwaitOptions<T>): T {
13
+ return use(toExternalHydrationThenable(opts.promise));
9
14
  }
@@ -7,9 +7,62 @@
7
7
  // in .tsrx so the compiler slots the internal useState/useEffect and callers'
8
8
  // withSlot wrapping keeps per-call-site state independent.
9
9
  import { useState, useEffect, isChildrenBlock } from 'octane';
10
+ import type { OctaneNode } from 'octane';
11
+ import type { BlockerFnArgs, HistoryAction, HistoryLocation } from '@tanstack/history';
10
12
  import { useRouter } from './context.ts';
11
13
 
12
- const IDLE_RESOLVER = {
14
+ // Blocker types, ported from react-router's useBlocker.tsx. Upstream derives the
15
+ // location shapes from the registered route tree's generics; the octane binding's
16
+ // route factories are untyped (`createRoute(options: any)`), so the union of
17
+ // matched-route shapes collapses to the loosely-typed equivalent.
18
+ export interface ShouldBlockFnLocation {
19
+ routeId: string;
20
+ fullPath: string;
21
+ pathname: string;
22
+ params: Record<string, string>;
23
+ search: Record<string, any>;
24
+ }
25
+
26
+ export type BlockerResolver =
27
+ | {
28
+ status: 'blocked';
29
+ current: ShouldBlockFnLocation;
30
+ next: ShouldBlockFnLocation;
31
+ action: HistoryAction;
32
+ proceed: () => void;
33
+ reset: () => void;
34
+ }
35
+ | {
36
+ status: 'idle';
37
+ current: undefined;
38
+ next: undefined;
39
+ action: undefined;
40
+ proceed: undefined;
41
+ reset: undefined;
42
+ };
43
+
44
+ export type ShouldBlockFnArgs = {
45
+ current: ShouldBlockFnLocation;
46
+ next: ShouldBlockFnLocation;
47
+ action: HistoryAction;
48
+ };
49
+
50
+ export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
51
+
52
+ export type UseBlockerOpts = {
53
+ shouldBlockFn: ShouldBlockFn;
54
+ enableBeforeUnload?: boolean | (() => boolean);
55
+ disabled?: boolean;
56
+ withResolver?: boolean;
57
+ };
58
+
59
+ type LegacyBlockerFn = () => Promise<any> | any;
60
+ type LegacyBlockerOpts = {
61
+ blockerFn?: LegacyBlockerFn;
62
+ condition?: boolean | any;
63
+ };
64
+
65
+ const IDLE_RESOLVER: BlockerResolver = {
13
66
  status: 'idle',
14
67
  current: undefined,
15
68
  next: undefined,
@@ -18,7 +71,10 @@ const IDLE_RESOLVER = {
18
71
  reset: undefined,
19
72
  };
20
73
 
21
- function _resolveBlockerOpts(opts, condition) {
74
+ function _resolveBlockerOpts(
75
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
76
+ condition?: boolean | any,
77
+ ): UseBlockerOpts {
22
78
  if (opts === undefined) {
23
79
  return { shouldBlockFn: () => true, withResolver: false };
24
80
  }
@@ -48,18 +104,21 @@ function _resolveBlockerOpts(opts, condition) {
48
104
  };
49
105
  }
50
106
 
51
- export function useBlocker(opts, condition) {
107
+ export function useBlocker(
108
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
109
+ condition?: boolean | any,
110
+ ): BlockerResolver {
52
111
  const { shouldBlockFn, enableBeforeUnload = true, disabled = false, withResolver = false } =
53
112
  _resolveBlockerOpts(opts, condition);
54
113
 
55
114
  const router = useRouter();
56
115
  const { history } = router;
57
116
 
58
- const [resolver, setResolver] = useState(IDLE_RESOLVER);
117
+ const [resolver, setResolver] = useState<BlockerResolver>(IDLE_RESOLVER);
59
118
 
60
119
  useEffect(() => {
61
- const blockerFnComposed = async (blockerFnArgs) => {
62
- function getLocation(location) {
120
+ const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
121
+ function getLocation(location: HistoryLocation): ShouldBlockFnLocation {
63
122
  const parsedLocation = router.parseLocation(location);
64
123
  const matchedRoutes = router.getMatchedRoutes(parsedLocation.pathname);
65
124
  if (matchedRoutes.foundRoute === undefined) {
@@ -95,7 +154,7 @@ export function useBlocker(opts, condition) {
95
154
  if (!withResolver) return shouldBlock;
96
155
  if (!shouldBlock) return false;
97
156
 
98
- const promise = new Promise((resolve) => {
157
+ const promise = new Promise<boolean>((resolve) => {
99
158
  setResolver({
100
159
  status: 'blocked',
101
160
  current,
@@ -119,9 +178,15 @@ export function useBlocker(opts, condition) {
119
178
  return resolver;
120
179
  }
121
180
 
181
+ // Upstream's PromptProps: the blocker options plus optional children (a render
182
+ // prop receiving the resolver, or plain renderables).
183
+ export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
184
+ children?: OctaneNode | ((params: BlockerResolver) => OctaneNode);
185
+ };
186
+
122
187
  // Declarative blocker: registers useBlocker and renders children (optionally a
123
188
  // render prop receiving the resolver).
124
- export function Block(props) @{
189
+ export function Block(props: PromptProps) @{
125
190
  const { children, ...rest } = props;
126
191
  const resolver = useBlocker(rest);
127
192
  const out =
@@ -1,3 +1,59 @@
1
1
  // Type declaration for the .tsrx module (resolved by relative path).
2
- export declare const useBlocker: (opts?: unknown, condition?: unknown) => unknown;
3
- export declare const Block: (props: Record<string, unknown>) => unknown;
2
+ import type { HistoryAction } from '@tanstack/history';
3
+
4
+ export interface ShouldBlockFnLocation {
5
+ routeId: string;
6
+ fullPath: string;
7
+ pathname: string;
8
+ params: Record<string, string>;
9
+ search: Record<string, any>;
10
+ }
11
+
12
+ export type BlockerResolver =
13
+ | {
14
+ status: 'blocked';
15
+ current: ShouldBlockFnLocation;
16
+ next: ShouldBlockFnLocation;
17
+ action: HistoryAction;
18
+ proceed: () => void;
19
+ reset: () => void;
20
+ }
21
+ | {
22
+ status: 'idle';
23
+ current: undefined;
24
+ next: undefined;
25
+ action: undefined;
26
+ proceed: undefined;
27
+ reset: undefined;
28
+ };
29
+
30
+ export type ShouldBlockFnArgs = {
31
+ current: ShouldBlockFnLocation;
32
+ next: ShouldBlockFnLocation;
33
+ action: HistoryAction;
34
+ };
35
+
36
+ export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
37
+
38
+ export type UseBlockerOpts = {
39
+ shouldBlockFn: ShouldBlockFn;
40
+ enableBeforeUnload?: boolean | (() => boolean);
41
+ disabled?: boolean;
42
+ withResolver?: boolean;
43
+ };
44
+
45
+ type LegacyBlockerFn = () => Promise<any> | any;
46
+ type LegacyBlockerOpts = {
47
+ blockerFn?: LegacyBlockerFn;
48
+ condition?: boolean | any;
49
+ };
50
+
51
+ export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
52
+ children?: unknown | ((params: BlockerResolver) => unknown);
53
+ };
54
+
55
+ export declare const useBlocker: (
56
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
57
+ condition?: boolean | any,
58
+ ) => BlockerResolver;
59
+ export declare const Block: (props: PromptProps) => unknown;
@@ -3,7 +3,27 @@
3
3
  import { useStore } from './useStore';
4
4
  import { useRouter } from './context';
5
5
  import { splitSlot, subSlot } from './internal';
6
+ import type { AnyRouter, RegisteredRouter, RouterState } from '@tanstack/router-core';
7
+ import type { StructuralSharingOption, ValidateSelected } from './structuralSharing';
6
8
 
9
+ export type UseRouterStateOptions<TRouter extends AnyRouter, TSelected, TStructuralSharing> = {
10
+ router?: TRouter;
11
+ select?: (
12
+ state: RouterState<TRouter['routeTree']>,
13
+ ) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
14
+ } & StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
15
+
16
+ export type UseRouterStateResult<TRouter extends AnyRouter, TSelected> = unknown extends TSelected
17
+ ? RouterState<TRouter['routeTree']>
18
+ : TSelected;
19
+
20
+ export function useRouterState<
21
+ TRouter extends AnyRouter = RegisteredRouter,
22
+ TSelected = unknown,
23
+ TStructuralSharing extends boolean = boolean,
24
+ >(
25
+ opts?: UseRouterStateOptions<TRouter, TSelected, TStructuralSharing>,
26
+ ): UseRouterStateResult<TRouter, TSelected>;
7
27
  export function useRouterState(...args: any[]): any {
8
28
  const [user, slot] = splitSlot(args);
9
29
  const opts = (user[0] ?? {}) as { select?: (s: any) => any; router?: any };
package/src/useStore.ts CHANGED
@@ -13,11 +13,22 @@ interface Atom<T> {
13
13
  get: () => T;
14
14
  }
15
15
 
16
- export function useStore<T, S = T>(...args: any[]): S {
16
+ // Public signature mirrors @tanstack/react-store's `useStore(store, selector,
17
+ // compare)`; the store parameter is the structural `{ get }` shape so router-core's
18
+ // `RouterReadableStore`/`RouterWritableStore` atoms (whose types omit `subscribe`)
19
+ // infer `T` directly. The trailing `slot` is the binding's forwarded call-site slot
20
+ // (see internal.ts) — the implementation splits it off the raw argument list.
21
+ export function useStore<T, S = T>(
22
+ atom: { get: () => T },
23
+ selector?: (state: T) => S,
24
+ compare?: (a: S, b: S) => boolean,
25
+ slot?: symbol,
26
+ ): S;
27
+ export function useStore(...args: any[]): any {
17
28
  const [user, slot] = splitSlot(args);
18
- const atom = user[0] as Atom<T>;
19
- const selector = (user[1] ?? ((s: T) => s as unknown as S)) as (s: T) => S;
20
- const compare = (user[2] ?? Object.is) as (a: S, b: S) => boolean;
29
+ const atom = user[0] as Atom<unknown>;
30
+ const selector = (user[1] ?? ((s: unknown) => s)) as (s: unknown) => unknown;
31
+ const compare = (user[2] ?? Object.is) as (a: unknown, b: unknown) => boolean;
21
32
 
22
33
  // Re-subscribe only when the atom identity changes (it's stable across renders).
23
34
  const subscribe = useCallback(
@@ -28,8 +39,8 @@ export function useStore<T, S = T>(...args: any[]): S {
28
39
 
29
40
  // Memoize selector output: same store input → same output; structurally-equal
30
41
  // output keeps its previous reference (so useSyncExternalStore doesn't loop).
31
- const cache = useRef<{ in: T; out: S } | null>(null, subSlot(slot, 'us:cache'));
32
- const getSnapshot = (): S => {
42
+ const cache = useRef<{ in: unknown; out: unknown } | null>(null, subSlot(slot, 'us:cache'));
43
+ const getSnapshot = (): unknown => {
33
44
  const input = atom.get();
34
45
  const prev = cache.current;
35
46
  if (prev && Object.is(prev.in, input)) return prev.out;