@octanejs/tanstack-router 0.2.6 → 0.2.8

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/src/hooks.ts CHANGED
@@ -1,375 +1,18 @@
1
- // The read hooks — ports of react-router's useMatch.tsx / useParams / useSearch /
2
- // useLoaderData / useLoaderDeps / useRouteContext / useNavigate / useCanGoBack /
3
- // Matches.tsx (useMatches / useParentMatches / useChildMatches). Everything match-
4
- // shaped funnels through `useMatch`, which subscribes to ONE match store via
5
- // `router.stores.getMatchStore(routeId)`:
6
- // - `from` given → that routeId's match store;
7
- // - no `from` → the NEAREST match via `matchContext` (the match id the enclosing
8
- // `<Match>` provided) — NOT the leaf match.
9
- // A missing match throws unless `shouldThrow: false` (upstream invariant).
10
- // Selectors run through `useStructuralSharing` (replaceEqualDeep against the
11
- // previous selection when `structuralSharing ?? defaultStructuralSharing`).
12
- import { useContext, useRef, useCallback } from 'octane';
13
- import { replaceEqualDeep } from '@tanstack/router-core';
1
+ import { useContext } from 'octane';
14
2
  import { useRouter, matchContext } from './context';
15
- import { useStore } from './useStore';
16
- import { splitSlot, subSlot } from './internal';
17
- import type {
18
- AnyRouter,
19
- FromPathOption,
20
- RegisteredRouter,
21
- ThrowConstraint,
22
- ThrowOrOptional,
23
- UseLoaderDataResult,
24
- UseLoaderDepsResult,
25
- UseNavigateResult,
26
- UseParamsResult,
27
- UseRouteContextOptions,
28
- UseRouteContextResult,
29
- UseSearchResult,
30
- } from '@tanstack/router-core';
31
- import type {
32
- UseLoaderDataOptions,
33
- UseLoaderDepsOptions,
34
- UseLocationBaseOptions,
35
- UseLocationResult,
36
- UseMatchOptions,
37
- UseMatchResult,
38
- UseMatchesBaseOptions,
39
- UseMatchesResult,
40
- UseParamsOptions,
41
- UseSearchOptions,
42
- } from './routeHookTypes';
43
- import type { StructuralSharingOption } from './structuralSharing';
44
-
45
- // Sentinel store + selection for "no match at this id" (upstream's dummyStore).
46
- const dummyStore = {
47
- get() {},
48
- subscribe() {
49
- return { unsubscribe() {} };
50
- },
51
- };
52
-
53
- // Selector wrapper honoring structural sharing: when enabled, the selection is
54
- // replaceEqualDeep'd against the previous one so deep-equal slices keep their
55
- // reference (no re-render). Port of react-router's useStructuralSharing.
56
- function useStructuralSharing(opts: any, router: any, slot: symbol | undefined) {
57
- const previousResult = useRef<any>(undefined, subSlot(slot, 'ss'));
58
- return (slice: any) => {
59
- const selected = opts?.select ? opts.select(slice) : slice;
60
- if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) {
61
- return (previousResult.current = replaceEqualDeep(previousResult.current, selected));
62
- }
63
- return selected;
64
- };
65
- }
66
-
67
- export function useMatch<
68
- TRouter extends AnyRouter = RegisteredRouter,
69
- const TFrom extends string | undefined = undefined,
70
- TStrict extends boolean = true,
71
- TThrow extends boolean = true,
72
- TSelected = unknown,
73
- TStructuralSharing extends boolean = boolean,
74
- >(
75
- opts: UseMatchOptions<
76
- TRouter,
77
- TFrom,
78
- TStrict,
79
- ThrowConstraint<TStrict, TThrow>,
80
- TSelected,
81
- TStructuralSharing
82
- >,
83
- ): ThrowOrOptional<UseMatchResult<TRouter, TFrom, TStrict, TSelected>, TThrow>;
84
- export function useMatch(opts: any, slot: symbol | undefined): any;
85
- export function useMatch(...args: any[]): any {
86
- const [user, slot] = splitSlot(args);
87
- const opts = user[0] ?? {};
88
- const router = useRouter();
89
- // octane has no rules of hooks, so the nearest-match context is read
90
- // unconditionally (upstream reads a dummy context when `from` is given).
91
- const nearestMatchId = useContext(matchContext);
92
- const routeId = opts.from ?? (nearestMatchId as string);
93
- const matchStore = router.stores.getMatchStore(routeId);
94
-
95
- const selector = useStructuralSharing(opts, router, subSlot(slot, 'm'));
96
- const matchSelection = useStore(
97
- matchStore ?? dummyStore,
98
- (match: any) => (match ? selector(match) : dummyStore),
99
- undefined,
100
- subSlot(slot, 'm:us'),
101
- );
102
-
103
- if (matchSelection !== dummyStore) return matchSelection;
104
- if (opts.shouldThrow ?? true) {
105
- throw new Error(
106
- `Invariant failed: Could not find ${
107
- opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'
108
- }`,
109
- );
110
- }
111
- return undefined;
112
- }
113
-
114
- export function useParams<
115
- TRouter extends AnyRouter = RegisteredRouter,
116
- const TFrom extends string | undefined = undefined,
117
- TStrict extends boolean = true,
118
- TThrow extends boolean = true,
119
- TSelected = unknown,
120
- TStructuralSharing extends boolean = boolean,
121
- >(
122
- opts: UseParamsOptions<
123
- TRouter,
124
- TFrom,
125
- TStrict,
126
- ThrowConstraint<TStrict, TThrow>,
127
- TSelected,
128
- TStructuralSharing
129
- >,
130
- ): ThrowOrOptional<UseParamsResult<TRouter, TFrom, TStrict, TSelected>, TThrow>;
131
- export function useParams(opts: any, slot: symbol | undefined): any;
132
- export function useParams(...args: any[]): any {
133
- const [user, slot] = splitSlot(args);
134
- const opts = user[0] ?? {};
135
- return useMatch(
136
- {
137
- from: opts.from,
138
- strict: opts.strict,
139
- shouldThrow: opts.shouldThrow,
140
- structuralSharing: opts.structuralSharing,
141
- select: (match: any) => {
142
- const params = opts.strict === false ? match.params : match._strictParams;
143
- return opts.select ? opts.select(params) : params;
144
- },
145
- },
146
- subSlot(slot, 'params'),
147
- );
148
- }
149
-
150
- export function useSearch<
151
- TRouter extends AnyRouter = RegisteredRouter,
152
- const TFrom extends string | undefined = undefined,
153
- TStrict extends boolean = true,
154
- TThrow extends boolean = true,
155
- TSelected = unknown,
156
- TStructuralSharing extends boolean = boolean,
157
- >(
158
- opts: UseSearchOptions<
159
- TRouter,
160
- TFrom,
161
- TStrict,
162
- ThrowConstraint<TStrict, TThrow>,
163
- TSelected,
164
- TStructuralSharing
165
- >,
166
- ): ThrowOrOptional<UseSearchResult<TRouter, TFrom, TStrict, TSelected>, TThrow>;
167
- export function useSearch(opts: any, slot: symbol | undefined): any;
168
- export function useSearch(...args: any[]): any {
169
- const [user, slot] = splitSlot(args);
170
- const opts = user[0] ?? {};
171
- return useMatch(
172
- {
173
- from: opts.from,
174
- strict: opts.strict,
175
- shouldThrow: opts.shouldThrow,
176
- structuralSharing: opts.structuralSharing,
177
- select: (match: any) => (opts.select ? opts.select(match.search) : match.search),
178
- },
179
- subSlot(slot, 'search'),
180
- );
181
- }
182
-
183
- export function useLoaderData<
184
- TRouter extends AnyRouter = RegisteredRouter,
185
- const TFrom extends string | undefined = undefined,
186
- TStrict extends boolean = true,
187
- TSelected = unknown,
188
- TStructuralSharing extends boolean = boolean,
189
- >(
190
- opts: UseLoaderDataOptions<TRouter, TFrom, TStrict, TSelected, TStructuralSharing>,
191
- ): UseLoaderDataResult<TRouter, TFrom, TStrict, TSelected>;
192
- export function useLoaderData(opts: any, slot: symbol | undefined): any;
193
- export function useLoaderData(...args: any[]): any {
194
- const [user, slot] = splitSlot(args);
195
- const opts = user[0] ?? {};
196
- return useMatch(
197
- {
198
- from: opts.from,
199
- strict: opts.strict,
200
- structuralSharing: opts.structuralSharing,
201
- select: (match: any) => (opts.select ? opts.select(match.loaderData) : match.loaderData),
202
- },
203
- subSlot(slot, 'loader'),
204
- );
205
- }
206
-
207
- export function useLoaderDeps<
208
- TRouter extends AnyRouter = RegisteredRouter,
209
- const TFrom extends string | undefined = undefined,
210
- TSelected = unknown,
211
- TStructuralSharing extends boolean = boolean,
212
- >(
213
- opts: UseLoaderDepsOptions<TRouter, TFrom, TSelected, TStructuralSharing>,
214
- ): UseLoaderDepsResult<TRouter, TFrom, TSelected>;
215
- export function useLoaderDeps(opts: any, slot: symbol | undefined): any;
216
- export function useLoaderDeps(...args: any[]): any {
217
- const [user, slot] = splitSlot(args);
218
- const opts = user[0] ?? {};
219
- const { select, ...rest } = opts;
220
- return useMatch(
221
- {
222
- ...rest,
223
- select: (match: any) => (select ? select(match.loaderDeps) : match.loaderDeps),
224
- },
225
- subSlot(slot, 'deps'),
226
- );
227
- }
228
-
229
- export function useRouteContext<
230
- TRouter extends AnyRouter = RegisteredRouter,
231
- const TFrom extends string | undefined = undefined,
232
- TStrict extends boolean = true,
233
- TSelected = unknown,
234
- >(
235
- opts: UseRouteContextOptions<TRouter, TFrom, TStrict, TSelected>,
236
- ): UseRouteContextResult<TRouter, TFrom, TStrict, TSelected>;
237
- export function useRouteContext(opts: any, slot: symbol | undefined): any;
238
- export function useRouteContext(...args: any[]): any {
239
- const [user, slot] = splitSlot(args);
240
- const opts = user[0] ?? {};
241
- return useMatch(
242
- {
243
- ...opts,
244
- select: (match: any) => (opts.select ? opts.select(match.context) : match.context),
245
- },
246
- subSlot(slot, 'ctx'),
247
- );
248
- }
249
-
250
- export function useLocation<
251
- TRouter extends AnyRouter = RegisteredRouter,
252
- TSelected = unknown,
253
- TStructuralSharing extends boolean = boolean,
254
- >(
255
- opts?: UseLocationBaseOptions<TRouter, TSelected, TStructuralSharing> &
256
- StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
257
- ): UseLocationResult<TRouter, TSelected>;
258
- export function useLocation(opts: any, slot: symbol | undefined): any;
259
- export function useLocation(...args: any[]): any {
260
- const [user, slot] = splitSlot(args);
261
- const opts = user[0] ?? {};
262
- const router = useRouter();
263
- return useStore(
264
- router.stores.location,
265
- useStructuralSharing(opts, router, subSlot(slot, 'loc')),
266
- undefined,
267
- subSlot(slot, 'loc:us'),
268
- );
269
- }
270
-
271
- export function useMatches<
272
- TRouter extends AnyRouter = RegisteredRouter,
273
- TSelected = unknown,
274
- TStructuralSharing extends boolean = boolean,
275
- >(
276
- opts?: UseMatchesBaseOptions<TRouter, TSelected, TStructuralSharing> &
277
- StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
278
- ): UseMatchesResult<TRouter, TSelected>;
279
- export function useMatches(opts: any, slot: symbol | undefined): any;
280
- export function useMatches(...args: any[]): any {
281
- const [user, slot] = splitSlot(args);
282
- const opts = user[0] ?? {};
283
- const router = useRouter();
284
- return useStore(
285
- router.stores.matches,
286
- useStructuralSharing(opts, router, subSlot(slot, 'matches')),
287
- undefined,
288
- subSlot(slot, 'matches:us'),
289
- );
290
- }
291
-
292
- export function useParentMatches<
293
- TRouter extends AnyRouter = RegisteredRouter,
294
- TSelected = unknown,
295
- TStructuralSharing extends boolean = boolean,
296
- >(
297
- opts?: UseMatchesBaseOptions<TRouter, TSelected, TStructuralSharing> &
298
- StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
299
- ): UseMatchesResult<TRouter, TSelected>;
300
- export function useParentMatches(opts: any, slot: symbol | undefined): any;
301
- export function useParentMatches(...args: any[]): any {
302
- const [user, slot] = splitSlot(args);
303
- const opts = user[0] ?? {};
304
- const contextRouteId = useContext(matchContext);
305
- return useMatches(
306
- {
307
- select: (matches: any[]) => {
308
- matches = matches.slice(
309
- 0,
310
- matches.findIndex((d: any) => d.routeId === contextRouteId),
311
- );
312
- return opts.select ? opts.select(matches) : matches;
313
- },
314
- structuralSharing: opts.structuralSharing,
315
- },
316
- subSlot(slot, 'parents'),
317
- );
318
- }
319
-
320
- export function useChildMatches<
321
- TRouter extends AnyRouter = RegisteredRouter,
322
- TSelected = unknown,
323
- TStructuralSharing extends boolean = boolean,
324
- >(
325
- opts?: UseMatchesBaseOptions<TRouter, TSelected, TStructuralSharing> &
326
- StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
327
- ): UseMatchesResult<TRouter, TSelected>;
328
- export function useChildMatches(opts: any, slot: symbol | undefined): any;
329
- export function useChildMatches(...args: any[]): any {
330
- const [user, slot] = splitSlot(args);
331
- const opts = user[0] ?? {};
332
- const contextRouteId = useContext(matchContext);
333
- return useMatches(
334
- {
335
- select: (matches: any[]) => {
336
- matches = matches.slice(matches.findIndex((d: any) => d.routeId === contextRouteId) + 1);
337
- return opts.select ? opts.select(matches) : matches;
338
- },
339
- structuralSharing: opts.structuralSharing,
340
- },
341
- subSlot(slot, 'children'),
342
- );
343
- }
344
-
345
- // Returns a STABLE navigate function (upstream useCallback([from, router])) that
346
- // forwards to `router.navigate`, defaulting `from` to the hook's option.
347
- export function useNavigate<
348
- TRouter extends AnyRouter = RegisteredRouter,
349
- TDefaultFrom extends string = string,
350
- >(options?: { from?: FromPathOption<TRouter, TDefaultFrom> }): UseNavigateResult<TDefaultFrom>;
351
- export function useNavigate(options: any, slot: symbol | undefined): (to: any) => any;
352
- export function useNavigate(...args: any[]): (to: any) => any {
353
- const [user, slot] = splitSlot(args);
354
- const opts = user[0] ?? {};
355
- const router = useRouter(opts.router ? { router: opts.router } : undefined);
356
- return useCallback(
357
- (options: any) => router.navigate({ ...options, from: options?.from ?? opts.from }),
358
- [opts.from, router],
359
- subSlot(slot, 'nav'),
360
- );
361
- }
362
-
363
- // True when the current history entry isn't the first (there is somewhere to go
364
- // back to) — per upstream useCanGoBack (location.state.__TSR_index !== 0).
365
- export function useCanGoBack(): boolean;
366
- export function useCanGoBack(...args: any[]): boolean {
367
- const [, slot] = splitSlot(args);
368
- const router = useRouter();
369
- return useStore(
370
- router.stores.location,
371
- (location: any) => location.state.__TSR_index !== 0,
372
- undefined,
373
- subSlot(slot, 'back'),
374
- );
375
- }
3
+ import { createRouterHooks } from './hookBindings';
4
+
5
+ export const {
6
+ useMatch,
7
+ useParams,
8
+ useSearch,
9
+ useLoaderData,
10
+ useLoaderDeps,
11
+ useRouteContext,
12
+ useLocation,
13
+ useMatches,
14
+ useParentMatches,
15
+ useChildMatches,
16
+ useNavigate,
17
+ useCanGoBack,
18
+ } = createRouterHooks(useRouter, () => useContext(matchContext));
package/src/index.ts CHANGED
@@ -166,6 +166,8 @@ export { Scripts } from './Scripts.tsrx';
166
166
  export { ScriptOnce } from './ScriptOnce.tsrx';
167
167
  export { Asset } from './Asset.tsrx';
168
168
  export type { AssetProps } from './Asset.tsrx';
169
+ export { getAssetKeys } from './assetKeys.ts';
170
+ export type { AssetKeys } from './assetKeys.ts';
169
171
  export { useTags } from './headContentUtils';
170
172
  export { Html } from './Html';
171
173
  export type { HtmlProps } from './Html';
@@ -1,57 +1,13 @@
1
- // Code-split a route's component: wrap a dynamic import as a component that suspends
2
- // (via octane's `use`) until the module loads, then renders it. Carries `.preload()`
3
- // for hover/intent preloading, and reloads the page once on a stale-chunk
4
- // module-not-found error (the same recovery react-router does).
5
- //
6
- // createRoute({ path: 'item/$id', component: lazyRouteComponent(() => import('./Item')) })
7
- import { use, createElement } from 'octane';
8
- import { isModuleNotFoundError } from '@tanstack/router-core';
9
- import { toExternalHydrationThenable } from './externalHydration';
1
+ // The normal entry accepts returned JSX and descriptors after loading.
2
+ import { createElement } from 'octane';
3
+ import { createLazyRouteLoader } from './lazyRouteLoader';
10
4
 
11
5
  export function lazyRouteComponent(
12
6
  importer: () => Promise<any>,
13
7
  exportName?: string,
14
8
  ): ((props: any) => any) & { preload: () => Promise<any> | undefined } {
15
- let loadPromise: Promise<any> | undefined;
16
- let comp: any;
17
- let error: any;
18
- let reload = false;
19
-
20
- const load = () => {
21
- if (!loadPromise) {
22
- loadPromise = importer()
23
- .then((res) => {
24
- loadPromise = undefined;
25
- comp = res[exportName ?? 'default'];
26
- })
27
- .catch((err) => {
28
- error = err;
29
- if (
30
- isModuleNotFoundError(error) &&
31
- error instanceof Error &&
32
- typeof window !== 'undefined' &&
33
- typeof sessionStorage !== 'undefined'
34
- ) {
35
- const key = `tanstack_router_reload:${error.message}`;
36
- if (!sessionStorage.getItem(key)) {
37
- sessionStorage.setItem(key, '1');
38
- reload = true;
39
- }
40
- }
41
- });
42
- }
43
- return loadPromise;
44
- };
45
-
46
- const Lazy = (props: any) => {
47
- if (reload) {
48
- window.location.reload();
49
- throw new Promise(() => {});
50
- }
51
- if (error) throw error;
52
- if (!comp) use(toExternalHydrationThenable(load()));
53
- return createElement(comp, props);
54
- };
55
- Lazy.preload = load;
9
+ const loader = createLazyRouteLoader(importer, exportName);
10
+ const Lazy = (props: any) => createElement(loader.read(), props);
11
+ Lazy.preload = loader.preload;
56
12
  return Lazy;
57
13
  }
@@ -0,0 +1,49 @@
1
+ // Shared import/preload and stale-module recovery. The renderer decides how to
2
+ // invoke the resolved component.
3
+ import { use } from 'octane';
4
+ import { isModuleNotFoundError } from '@tanstack/router-core';
5
+ import { toExternalHydrationThenable } from './externalHydration';
6
+
7
+ export function createLazyRouteLoader(importer: () => Promise<any>, exportName?: string) {
8
+ let loadPromise: Promise<any> | undefined;
9
+ let comp: any;
10
+ let error: any;
11
+ let reload = false;
12
+ const preload = () => {
13
+ if (!loadPromise) {
14
+ loadPromise = importer()
15
+ .then((res) => {
16
+ loadPromise = undefined;
17
+ comp = res[exportName ?? 'default'];
18
+ })
19
+ .catch((err) => {
20
+ error = err;
21
+ if (
22
+ isModuleNotFoundError(error) &&
23
+ error instanceof Error &&
24
+ typeof window !== 'undefined' &&
25
+ typeof sessionStorage !== 'undefined'
26
+ ) {
27
+ const key = `tanstack_router_reload:${error.message}`;
28
+ if (!sessionStorage.getItem(key)) {
29
+ sessionStorage.setItem(key, '1');
30
+ reload = true;
31
+ }
32
+ }
33
+ });
34
+ }
35
+ return loadPromise;
36
+ };
37
+ return {
38
+ preload,
39
+ read() {
40
+ if (reload) {
41
+ window.location.reload();
42
+ throw new Promise(() => {});
43
+ }
44
+ if (error) throw error;
45
+ if (!comp) use(toExternalHydrationThenable(preload()));
46
+ return comp;
47
+ },
48
+ };
49
+ }