@octanejs/tanstack-router 0.2.8 → 0.2.10
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 +42 -22
- package/package.json +2 -2
- package/src/Asset.tsrx +3 -135
- package/src/ClientOnly.tsrx +2 -0
- package/src/Link.tsrx +2 -53
- package/src/Matches.tsrx +6 -4
- package/src/Scripts.tsrx +2 -19
- package/src/Transitioner.tsrx +5 -4
- package/src/assetComponent.tsrx +146 -0
- package/src/compiled/Document.tsrx +101 -0
- package/src/compiled/Link.tsrx +18 -0
- package/src/compiled/Matches.tsrx +28 -19
- package/src/compiled/bindings.ts +2 -0
- package/src/compiled/route.tsrx +71 -0
- package/src/compiled.ts +36 -1
- package/src/fileRoute.ts +7 -58
- package/src/headContentBinding.ts +178 -0
- package/src/headContentUtils.ts +2 -170
- package/src/hookBindings.ts +2 -0
- package/src/internal.ts +2 -0
- package/src/link.ts +5 -379
- package/src/linkBinding.ts +391 -0
- package/src/linkComponent.tsrx +61 -0
- package/src/route.ts +6 -51
- package/src/routeHookBindings.ts +146 -0
- package/src/routerStateBinding.ts +2 -0
- package/src/scriptComponent.tsrx +27 -0
- package/src/scriptContentBinding.ts +43 -0
- package/src/scriptContentUtils.ts +2 -35
- package/src/useStore.ts +2 -0
|
@@ -36,15 +36,23 @@ export function CompiledRouterProvider(props: RouterProps) @{
|
|
|
36
36
|
</CompiledContext>
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function CompiledWrap(props: { component?: any; children?: unknown }) @{
|
|
40
|
+
@if (props.component) {
|
|
41
|
+
<CompiledComponent component={props.component} props={{ children: props.children }} />
|
|
42
|
+
} @else {
|
|
43
|
+
<>{props.children}</>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function CompiledMatches() @{
|
|
40
48
|
const router = useRouter();
|
|
41
49
|
const rootRoute = (router.routesById as any)[rootRouteId];
|
|
42
50
|
useLayoutEffect(() => {
|
|
43
51
|
if (router.options.scrollRestoration) setupScrollRestoration(router);
|
|
44
52
|
}, [router]);
|
|
45
|
-
useTransitioner(router);
|
|
53
|
+
const expectedMatches = useTransitioner(router);
|
|
46
54
|
@try {
|
|
47
|
-
<CompiledMatchesInner />
|
|
55
|
+
<CompiledMatchesInner expectedMatches={expectedMatches} />
|
|
48
56
|
} @pending {
|
|
49
57
|
<CompiledPending route={rootRoute} />
|
|
50
58
|
} @catch (error, reset) {
|
|
@@ -53,16 +61,20 @@ function CompiledMatches() @{
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
function CompiledMatchesInner() @{
|
|
64
|
+
function CompiledMatchesInner(props: { expectedMatches: unknown }) @{
|
|
57
65
|
const router = useRouter();
|
|
58
66
|
const acknowledgement: Array<any> = ((router as any)._rendered ??= []);
|
|
59
|
-
const matches = useStore(router.stores.matches, (m: any) =>
|
|
67
|
+
const matches = useStore(router.stores.matches, (m: any) =>
|
|
68
|
+
acknowledgement[0] === props.expectedMatches ? (props.expectedMatches ?? m) : m,
|
|
69
|
+
) as any;
|
|
60
70
|
const rootMatch = matches[0];
|
|
61
71
|
useLayoutEffect(() => {
|
|
62
72
|
if (acknowledgement[0] === matches) settleOwner(acknowledgement, true);
|
|
63
73
|
}, [acknowledgement, matches]);
|
|
64
74
|
@if (rootMatch) {
|
|
65
|
-
<
|
|
75
|
+
<CompiledContext context={matchContext} value={rootMatch.routeId}>
|
|
76
|
+
<CompiledMatch routeId={rootMatch.routeId} />
|
|
77
|
+
</CompiledContext>
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -71,31 +83,28 @@ function CompiledMatch(props: { routeId: string }) @{
|
|
|
71
83
|
const store = router.stores.getMatchStore(props.routeId);
|
|
72
84
|
const ssr = useStore(store, (match: any) => match.ssr);
|
|
73
85
|
const route = (router.routesById as Record<string, any>)[props.routeId];
|
|
74
|
-
if (route.isRoot && route.options.shellComponent) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
'CompiledRouterProvider does not accept shellComponent. Use RouterProvider for renderable document shells.',
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
86
|
const pending = route.options.pendingComponent ?? router.options.defaultPendingComponent;
|
|
80
87
|
const errorComponent = route.options.errorComponent ?? router.options.defaultErrorComponent;
|
|
81
88
|
const noSsr = ssr === false || ssr === 'data-only';
|
|
82
89
|
const hasSuspense =
|
|
83
90
|
(!route.isRoot || route.options.wrapInSuspense || noSsr) &&
|
|
84
91
|
((route.options.wrapInSuspense ?? pending ?? errorComponent?.preload) || noSsr);
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
@
|
|
92
|
+
<CompiledWrap component={route.isRoot ? route.options.shellComponent : undefined}>
|
|
93
|
+
<CompiledContext context={matchContext} value={props.routeId}>
|
|
94
|
+
@if (hasSuspense) {
|
|
95
|
+
@try {
|
|
96
|
+
<CompiledMatchContent route={route} routeId={props.routeId} noSsr={noSsr} />
|
|
97
|
+
} @pending {
|
|
98
|
+
<CompiledPending route={route} />
|
|
99
|
+
}
|
|
100
|
+
} @else {
|
|
88
101
|
<CompiledMatchContent route={route} routeId={props.routeId} noSsr={noSsr} />
|
|
89
|
-
} @pending {
|
|
90
|
-
<CompiledPending route={route} />
|
|
91
102
|
}
|
|
92
|
-
|
|
93
|
-
<CompiledMatchContent route={route} routeId={props.routeId} noSsr={noSsr} />
|
|
94
|
-
}
|
|
103
|
+
</CompiledContext>
|
|
95
104
|
@if (route.parentRoute?.id === rootRouteId && router.options.scrollRestoration) {
|
|
96
105
|
<CompiledScrollRestoration />
|
|
97
106
|
}
|
|
98
|
-
</
|
|
107
|
+
</CompiledWrap>
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
// Without a configured Suspense boundary, both lazy imports and loader promises
|
package/src/compiled/bindings.ts
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strong';
|
|
2
|
+
|
|
3
|
+
import { BaseRoute, BaseRootRoute, BaseRouteApi, notFound } from '@tanstack/router-core';
|
|
4
|
+
import { createRouteHookBindings } from '../routeHookBindings';
|
|
5
|
+
import * as hooks from './bindings';
|
|
6
|
+
import { Link } from './Link.tsrx';
|
|
7
|
+
import { splitSlot, subSlot } from '../internal';
|
|
8
|
+
import type * as RouteFactories from '../route';
|
|
9
|
+
import type * as FileFactories from '../fileRoute';
|
|
10
|
+
|
|
11
|
+
const { attachRouteHooks, attachRouteNavigation, attachLazyRouteHooks } = createRouteHookBindings(
|
|
12
|
+
hooks,
|
|
13
|
+
hooks.useRouter,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function attachRoute(route: any) {
|
|
17
|
+
attachRouteHooks(route, true);
|
|
18
|
+
attachRouteNavigation(route);
|
|
19
|
+
function RouteLink(props: any) @{
|
|
20
|
+
<Link from={route.fullPath} {...props} />
|
|
21
|
+
}
|
|
22
|
+
route.Link = RouteLink;
|
|
23
|
+
return route;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// These factories preserve the binding's precise route types, while constructing
|
|
27
|
+
// core instances directly: no ordinary-context hooks or descriptor Link are made.
|
|
28
|
+
export const createRoute: typeof RouteFactories.createRoute = (options: any) =>
|
|
29
|
+
attachRoute(new BaseRoute(options));
|
|
30
|
+
export const createRootRoute: typeof RouteFactories.createRootRoute = (options?: any) =>
|
|
31
|
+
attachRoute(new BaseRootRoute(options));
|
|
32
|
+
export const createRootRouteWithContext: typeof RouteFactories.createRootRouteWithContext = () =>
|
|
33
|
+
createRootRoute;
|
|
34
|
+
export const rootRouteWithContext = createRootRouteWithContext;
|
|
35
|
+
|
|
36
|
+
export const getRouteApi: typeof RouteFactories.getRouteApi = (id: any) => {
|
|
37
|
+
const route = new BaseRouteApi({ id }) as any;
|
|
38
|
+
attachRouteHooks(route, false);
|
|
39
|
+
route.useNavigate = (...args: any[]) => {
|
|
40
|
+
const [, slot] = splitSlot(args);
|
|
41
|
+
const router = hooks.useRouter();
|
|
42
|
+
return hooks.useNavigate(
|
|
43
|
+
{ from: (router.routesById as any)[id].fullPath },
|
|
44
|
+
subSlot(slot, 'r:n'),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
function RouteLink(props: any) @{
|
|
48
|
+
const router = hooks.useRouter();
|
|
49
|
+
<Link from={(router.routesById as any)[id].fullPath} {...props} />
|
|
50
|
+
}
|
|
51
|
+
route.Link = RouteLink;
|
|
52
|
+
route.notFound = (options: any) => notFound({ routeId: id, ...options });
|
|
53
|
+
return route;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const createFileRoute: typeof FileFactories.createFileRoute = () => (options: any) => {
|
|
57
|
+
const route = createRoute(options) as any;
|
|
58
|
+
route.isRoot = false;
|
|
59
|
+
return route;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function lazyRoute(options: any) {
|
|
63
|
+
const route = { options } as any;
|
|
64
|
+
attachLazyRouteHooks(route);
|
|
65
|
+
return route;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const createLazyRoute: typeof FileFactories.createLazyRoute = (id: any) => (options: any) =>
|
|
69
|
+
lazyRoute({ id, ...options });
|
|
70
|
+
export const createLazyFileRoute: typeof FileFactories.createLazyFileRoute = (id: any) =>
|
|
71
|
+
typeof id === 'object' ? lazyRoute(id) : (options: any) => lazyRoute({ id, ...options });
|
package/src/compiled.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Opt-in source-owned renderer for
|
|
1
|
+
// Opt-in source-owned renderer for route trees with compiled bodies.
|
|
2
2
|
import './frameworkTypes';
|
|
3
3
|
export * from '@tanstack/router-core';
|
|
4
4
|
export { Router, createRouter } from './router';
|
|
@@ -8,6 +8,17 @@ export {
|
|
|
8
8
|
createHashHistory,
|
|
9
9
|
createMemoryHistory,
|
|
10
10
|
} from './history';
|
|
11
|
+
export type {
|
|
12
|
+
RouterHistory,
|
|
13
|
+
HistoryLocation,
|
|
14
|
+
ParsedPath,
|
|
15
|
+
HistoryState,
|
|
16
|
+
ParsedHistoryState,
|
|
17
|
+
HistoryAction,
|
|
18
|
+
BlockerFnArgs,
|
|
19
|
+
BlockerFn,
|
|
20
|
+
NavigationBlocker,
|
|
21
|
+
} from '@tanstack/history';
|
|
11
22
|
export {
|
|
12
23
|
useRouter,
|
|
13
24
|
useRouterState,
|
|
@@ -27,6 +38,30 @@ export {
|
|
|
27
38
|
export type { UseRouterStateOptions, UseRouterStateResult } from './routerStateBinding';
|
|
28
39
|
export {
|
|
29
40
|
CompiledRouterProvider,
|
|
41
|
+
CompiledRouterProvider as RouterProvider,
|
|
30
42
|
CompiledOutlet,
|
|
43
|
+
CompiledOutlet as Outlet,
|
|
31
44
|
lazyCompiledRouteComponent,
|
|
45
|
+
lazyCompiledRouteComponent as lazyRouteComponent,
|
|
32
46
|
} from './compiled/Matches.tsrx';
|
|
47
|
+
export {
|
|
48
|
+
createRoute,
|
|
49
|
+
createRootRoute,
|
|
50
|
+
createRootRouteWithContext,
|
|
51
|
+
rootRouteWithContext,
|
|
52
|
+
getRouteApi,
|
|
53
|
+
createFileRoute,
|
|
54
|
+
createLazyRoute,
|
|
55
|
+
createLazyFileRoute,
|
|
56
|
+
} from './compiled/route.tsrx';
|
|
57
|
+
export { Link, useLinkProps, createLink } from './compiled/Link.tsrx';
|
|
58
|
+
export { ClientOnly, useHydrated } from './ClientOnly.tsrx';
|
|
59
|
+
export { getAssetKeys } from './assetKeys';
|
|
60
|
+
export { Html, Head, Body, Asset, HeadContent, Scripts, useTags } from './compiled/Document.tsrx';
|
|
61
|
+
export type { AssetKeys } from './assetKeys';
|
|
62
|
+
export type {
|
|
63
|
+
LinkComponent,
|
|
64
|
+
LinkComponentProps,
|
|
65
|
+
LinkProps,
|
|
66
|
+
UseLinkPropsOptions,
|
|
67
|
+
} from './linkTypes';
|
package/src/fileRoute.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
useSearch,
|
|
11
11
|
} from './hooks';
|
|
12
12
|
import { useRouter } from './context';
|
|
13
|
-
import {
|
|
13
|
+
import { createRouteHookBindings } from './routeHookBindings';
|
|
14
|
+
|
|
15
|
+
const { attachLazyRouteHooks } = createRouteHookBindings(
|
|
16
|
+
{ useMatch, useParams, useSearch, useLoaderData, useLoaderDeps, useRouteContext, useNavigate },
|
|
17
|
+
useRouter,
|
|
18
|
+
);
|
|
14
19
|
import type {
|
|
15
20
|
AnyContext,
|
|
16
21
|
AnyRoute,
|
|
@@ -198,63 +203,7 @@ export class LazyRoute<TRoute extends AnyRoute> {
|
|
|
198
203
|
|
|
199
204
|
constructor(opts: { id: string } & LazyRouteOptions) {
|
|
200
205
|
this.options = opts;
|
|
201
|
-
|
|
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;
|
|
206
|
+
attachLazyRouteHooks(this);
|
|
258
207
|
}
|
|
259
208
|
}
|
|
260
209
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strong';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendUniqueUserTags,
|
|
5
|
+
deepEqual,
|
|
6
|
+
escapeHtml,
|
|
7
|
+
getAssetCrossOrigin,
|
|
8
|
+
getScriptPreloadAttrs,
|
|
9
|
+
resolveManifestCssLink,
|
|
10
|
+
} from '@tanstack/router-core';
|
|
11
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
12
|
+
import type { useRouter as UseRouter } from './context';
|
|
13
|
+
import { splitSlot, subSlot } from './internal';
|
|
14
|
+
import { useStore } from './useStore';
|
|
15
|
+
import type {
|
|
16
|
+
AnyRouteMatch,
|
|
17
|
+
AnyRouter,
|
|
18
|
+
AssetCrossOriginConfig,
|
|
19
|
+
RouterManagedTag,
|
|
20
|
+
} from '@tanstack/router-core';
|
|
21
|
+
|
|
22
|
+
function buildTagsFromMatches(
|
|
23
|
+
router: AnyRouter,
|
|
24
|
+
nonce: string | undefined,
|
|
25
|
+
matches: Array<AnyRouteMatch>,
|
|
26
|
+
assetCrossOrigin?: AssetCrossOriginConfig,
|
|
27
|
+
): Array<RouterManagedTag> {
|
|
28
|
+
const routeMeta = matches.map((match) => match.meta).filter((meta) => meta !== undefined);
|
|
29
|
+
|
|
30
|
+
const resultMeta: Array<RouterManagedTag> = [];
|
|
31
|
+
const metaByAttribute: Record<string, true> = {};
|
|
32
|
+
let title: RouterManagedTag | undefined;
|
|
33
|
+
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
34
|
+
const metas = routeMeta[i]!;
|
|
35
|
+
for (let j = metas.length - 1; j >= 0; j--) {
|
|
36
|
+
const meta = metas[j];
|
|
37
|
+
if (!meta) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if ('title' in meta && typeof meta.title === 'string') {
|
|
42
|
+
title ??= { tag: 'title', children: meta.title };
|
|
43
|
+
} else if ('script:ld+json' in meta) {
|
|
44
|
+
try {
|
|
45
|
+
resultMeta.push({
|
|
46
|
+
tag: 'script',
|
|
47
|
+
attrs: { type: 'application/ld+json' },
|
|
48
|
+
children: escapeHtml(JSON.stringify(meta['script:ld+json'])),
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore values that cannot be serialized as JSON-LD.
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
const attribute =
|
|
55
|
+
('name' in meta && typeof meta.name === 'string' ? meta.name : undefined) ??
|
|
56
|
+
('property' in meta && typeof meta.property === 'string' ? meta.property : undefined);
|
|
57
|
+
if (attribute && metaByAttribute[attribute]) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (attribute) {
|
|
61
|
+
metaByAttribute[attribute] = true;
|
|
62
|
+
}
|
|
63
|
+
resultMeta.push({ tag: 'meta', attrs: { ...meta, nonce } });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (title) {
|
|
69
|
+
resultMeta.push(title);
|
|
70
|
+
}
|
|
71
|
+
if (nonce) {
|
|
72
|
+
resultMeta.push({
|
|
73
|
+
tag: 'meta',
|
|
74
|
+
attrs: { property: 'csp-nonce', content: nonce },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
resultMeta.reverse();
|
|
78
|
+
|
|
79
|
+
const links = matches
|
|
80
|
+
.flatMap((match) => match.links ?? [])
|
|
81
|
+
.filter((link) => link !== undefined)
|
|
82
|
+
.map((link) => ({ tag: 'link', attrs: { ...link, nonce } }) satisfies RouterManagedTag);
|
|
83
|
+
|
|
84
|
+
const manifestTags: Array<RouterManagedTag> = [];
|
|
85
|
+
const preloadTags: Array<RouterManagedTag> = [];
|
|
86
|
+
const manifest = router.ssr?.manifest;
|
|
87
|
+
if (manifest) {
|
|
88
|
+
for (const match of matches) {
|
|
89
|
+
for (const link of manifest.routes[match.routeId]?.css ?? []) {
|
|
90
|
+
const resolvedLink = resolveManifestCssLink(link);
|
|
91
|
+
manifestTags.push({
|
|
92
|
+
tag: 'link',
|
|
93
|
+
attrs: {
|
|
94
|
+
rel: 'stylesheet',
|
|
95
|
+
...resolvedLink,
|
|
96
|
+
crossOrigin:
|
|
97
|
+
getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ?? resolvedLink.crossOrigin,
|
|
98
|
+
nonce,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
for (const preload of manifest.routes[match.routeId]?.preloads ?? []) {
|
|
103
|
+
preloadTags.push({
|
|
104
|
+
tag: 'link',
|
|
105
|
+
attrs: {
|
|
106
|
+
...getScriptPreloadAttrs(manifest, preload, assetCrossOrigin),
|
|
107
|
+
nonce,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (manifest.inlineStyle) {
|
|
114
|
+
manifestTags.push({
|
|
115
|
+
tag: 'style',
|
|
116
|
+
attrs: { ...manifest.inlineStyle.attrs, nonce },
|
|
117
|
+
children: manifest.inlineStyle.children,
|
|
118
|
+
inlineCss: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const styles = matches
|
|
124
|
+
.flatMap((match) => match.styles ?? [])
|
|
125
|
+
.filter((style) => style !== undefined)
|
|
126
|
+
.map(
|
|
127
|
+
({ children, ...attrs }) =>
|
|
128
|
+
({
|
|
129
|
+
tag: 'style',
|
|
130
|
+
attrs: { ...attrs, nonce },
|
|
131
|
+
children: children,
|
|
132
|
+
}) satisfies RouterManagedTag,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const headScripts = matches
|
|
136
|
+
.flatMap((match) => match.headScripts ?? [])
|
|
137
|
+
.filter((script) => script !== undefined)
|
|
138
|
+
.map(
|
|
139
|
+
({ children, ...attrs }) =>
|
|
140
|
+
({
|
|
141
|
+
tag: 'script',
|
|
142
|
+
attrs: { ...attrs, nonce },
|
|
143
|
+
children: children,
|
|
144
|
+
}) satisfies RouterManagedTag,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const tags: Array<RouterManagedTag> = [];
|
|
148
|
+
appendUniqueUserTags(tags, resultMeta);
|
|
149
|
+
tags.push(...preloadTags);
|
|
150
|
+
appendUniqueUserTags(tags, links);
|
|
151
|
+
tags.push(...manifestTags);
|
|
152
|
+
appendUniqueUserTags(tags, styles);
|
|
153
|
+
appendUniqueUserTags(tags, headScripts);
|
|
154
|
+
return tags;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createUseTags(useRouter: typeof UseRouter) {
|
|
158
|
+
function useTags(...args: Array<unknown>): Array<RouterManagedTag> {
|
|
159
|
+
const [userArgs, slot] = splitSlot(args);
|
|
160
|
+
const assetCrossOrigin = userArgs[0] as AssetCrossOriginConfig | undefined;
|
|
161
|
+
const router = useRouter();
|
|
162
|
+
const nonce = router.options.ssr?.nonce;
|
|
163
|
+
|
|
164
|
+
if (isServer ?? router.isServer) {
|
|
165
|
+
return buildTagsFromMatches(router, nonce, router.stores.matches.get(), assetCrossOrigin);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return useStore(
|
|
169
|
+
router.stores.matches,
|
|
170
|
+
(matches: Array<AnyRouteMatch>) =>
|
|
171
|
+
buildTagsFromMatches(router, nonce, matches, assetCrossOrigin),
|
|
172
|
+
deepEqual,
|
|
173
|
+
subSlot(slot, 'head:tags'),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return useTags;
|
|
178
|
+
}
|
package/src/headContentUtils.ts
CHANGED
|
@@ -1,172 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendUniqueUserTags,
|
|
3
|
-
deepEqual,
|
|
4
|
-
escapeHtml,
|
|
5
|
-
getAssetCrossOrigin,
|
|
6
|
-
getScriptPreloadAttrs,
|
|
7
|
-
resolveManifestCssLink,
|
|
8
|
-
} from '@tanstack/router-core';
|
|
9
|
-
import { isServer } from '@tanstack/router-core/isServer';
|
|
10
1
|
import { useRouter } from './context';
|
|
11
|
-
import {
|
|
12
|
-
import { useStore } from './useStore';
|
|
13
|
-
import type {
|
|
14
|
-
AnyRouteMatch,
|
|
15
|
-
AnyRouter,
|
|
16
|
-
AssetCrossOriginConfig,
|
|
17
|
-
RouterManagedTag,
|
|
18
|
-
} from '@tanstack/router-core';
|
|
2
|
+
import { createUseTags } from './headContentBinding';
|
|
19
3
|
|
|
20
|
-
|
|
21
|
-
router: AnyRouter,
|
|
22
|
-
nonce: string | undefined,
|
|
23
|
-
matches: Array<AnyRouteMatch>,
|
|
24
|
-
assetCrossOrigin?: AssetCrossOriginConfig,
|
|
25
|
-
): Array<RouterManagedTag> {
|
|
26
|
-
const routeMeta = matches.map((match) => match.meta).filter((meta) => meta !== undefined);
|
|
27
|
-
|
|
28
|
-
const resultMeta: Array<RouterManagedTag> = [];
|
|
29
|
-
const metaByAttribute: Record<string, true> = {};
|
|
30
|
-
let title: RouterManagedTag | undefined;
|
|
31
|
-
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
32
|
-
const metas = routeMeta[i]!;
|
|
33
|
-
for (let j = metas.length - 1; j >= 0; j--) {
|
|
34
|
-
const meta = metas[j];
|
|
35
|
-
if (!meta) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if ('title' in meta && typeof meta.title === 'string') {
|
|
40
|
-
title ??= { tag: 'title', children: meta.title };
|
|
41
|
-
} else if ('script:ld+json' in meta) {
|
|
42
|
-
try {
|
|
43
|
-
resultMeta.push({
|
|
44
|
-
tag: 'script',
|
|
45
|
-
attrs: { type: 'application/ld+json' },
|
|
46
|
-
children: escapeHtml(JSON.stringify(meta['script:ld+json'])),
|
|
47
|
-
});
|
|
48
|
-
} catch {
|
|
49
|
-
// Ignore values that cannot be serialized as JSON-LD.
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
const attribute =
|
|
53
|
-
('name' in meta && typeof meta.name === 'string' ? meta.name : undefined) ??
|
|
54
|
-
('property' in meta && typeof meta.property === 'string' ? meta.property : undefined);
|
|
55
|
-
if (attribute && metaByAttribute[attribute]) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (attribute) {
|
|
59
|
-
metaByAttribute[attribute] = true;
|
|
60
|
-
}
|
|
61
|
-
resultMeta.push({ tag: 'meta', attrs: { ...meta, nonce } });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (title) {
|
|
67
|
-
resultMeta.push(title);
|
|
68
|
-
}
|
|
69
|
-
if (nonce) {
|
|
70
|
-
resultMeta.push({
|
|
71
|
-
tag: 'meta',
|
|
72
|
-
attrs: { property: 'csp-nonce', content: nonce },
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
resultMeta.reverse();
|
|
76
|
-
|
|
77
|
-
const links = matches
|
|
78
|
-
.flatMap((match) => match.links ?? [])
|
|
79
|
-
.filter((link) => link !== undefined)
|
|
80
|
-
.map((link) => ({ tag: 'link', attrs: { ...link, nonce } }) satisfies RouterManagedTag);
|
|
81
|
-
|
|
82
|
-
const manifestTags: Array<RouterManagedTag> = [];
|
|
83
|
-
const preloadTags: Array<RouterManagedTag> = [];
|
|
84
|
-
const manifest = router.ssr?.manifest;
|
|
85
|
-
if (manifest) {
|
|
86
|
-
for (const match of matches) {
|
|
87
|
-
for (const link of manifest.routes[match.routeId]?.css ?? []) {
|
|
88
|
-
const resolvedLink = resolveManifestCssLink(link);
|
|
89
|
-
manifestTags.push({
|
|
90
|
-
tag: 'link',
|
|
91
|
-
attrs: {
|
|
92
|
-
rel: 'stylesheet',
|
|
93
|
-
...resolvedLink,
|
|
94
|
-
crossOrigin:
|
|
95
|
-
getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ?? resolvedLink.crossOrigin,
|
|
96
|
-
nonce,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
for (const preload of manifest.routes[match.routeId]?.preloads ?? []) {
|
|
101
|
-
preloadTags.push({
|
|
102
|
-
tag: 'link',
|
|
103
|
-
attrs: {
|
|
104
|
-
...getScriptPreloadAttrs(manifest, preload, assetCrossOrigin),
|
|
105
|
-
nonce,
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (manifest.inlineStyle) {
|
|
112
|
-
manifestTags.push({
|
|
113
|
-
tag: 'style',
|
|
114
|
-
attrs: { ...manifest.inlineStyle.attrs, nonce },
|
|
115
|
-
children: manifest.inlineStyle.children,
|
|
116
|
-
inlineCss: true,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const styles = matches
|
|
122
|
-
.flatMap((match) => match.styles ?? [])
|
|
123
|
-
.filter((style) => style !== undefined)
|
|
124
|
-
.map(
|
|
125
|
-
({ children, ...attrs }) =>
|
|
126
|
-
({
|
|
127
|
-
tag: 'style',
|
|
128
|
-
attrs: { ...attrs, nonce },
|
|
129
|
-
children: children,
|
|
130
|
-
}) satisfies RouterManagedTag,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const headScripts = matches
|
|
134
|
-
.flatMap((match) => match.headScripts ?? [])
|
|
135
|
-
.filter((script) => script !== undefined)
|
|
136
|
-
.map(
|
|
137
|
-
({ children, ...attrs }) =>
|
|
138
|
-
({
|
|
139
|
-
tag: 'script',
|
|
140
|
-
attrs: { ...attrs, nonce },
|
|
141
|
-
children: children,
|
|
142
|
-
}) satisfies RouterManagedTag,
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const tags: Array<RouterManagedTag> = [];
|
|
146
|
-
appendUniqueUserTags(tags, resultMeta);
|
|
147
|
-
tags.push(...preloadTags);
|
|
148
|
-
appendUniqueUserTags(tags, links);
|
|
149
|
-
tags.push(...manifestTags);
|
|
150
|
-
appendUniqueUserTags(tags, styles);
|
|
151
|
-
appendUniqueUserTags(tags, headScripts);
|
|
152
|
-
return tags;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function useTags(...args: Array<unknown>): Array<RouterManagedTag> {
|
|
156
|
-
const [userArgs, slot] = splitSlot(args);
|
|
157
|
-
const assetCrossOrigin = userArgs[0] as AssetCrossOriginConfig | undefined;
|
|
158
|
-
const router = useRouter();
|
|
159
|
-
const nonce = router.options.ssr?.nonce;
|
|
160
|
-
|
|
161
|
-
if (isServer ?? router.isServer) {
|
|
162
|
-
return buildTagsFromMatches(router, nonce, router.stores.matches.get(), assetCrossOrigin);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return useStore(
|
|
166
|
-
router.stores.matches,
|
|
167
|
-
(matches: Array<AnyRouteMatch>) =>
|
|
168
|
-
buildTagsFromMatches(router, nonce, matches, assetCrossOrigin),
|
|
169
|
-
deepEqual,
|
|
170
|
-
subSlot(slot, 'head:tags'),
|
|
171
|
-
);
|
|
172
|
-
}
|
|
4
|
+
export const useTags = createUseTags(useRouter);
|
package/src/hookBindings.ts
CHANGED
package/src/internal.ts
CHANGED