@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.
- package/README.md +24 -11
- package/package.json +22 -7
- package/src/Asset.tsrx +121 -0
- package/src/Asset.tsrx.d.ts +9 -0
- package/src/Await.tsrx +9 -3
- package/src/Await.tsrx.d.ts +8 -6
- package/src/Body.ts +31 -0
- package/src/CatchBoundary.tsrx +21 -4
- package/src/CatchBoundary.tsrx.d.ts +10 -4
- package/src/ClientOnly.tsrx +6 -3
- package/src/Head.ts +22 -0
- package/src/HeadContent.tsrx +41 -0
- package/src/HeadContent.tsrx.d.ts +8 -0
- package/src/Html.ts +19 -0
- package/src/Link.tsrx +19 -4
- package/src/Link.tsrx.d.ts +3 -4
- package/src/Match.tsrx +61 -29
- package/src/MatchRoute.tsrx +6 -3
- package/src/Matches.tsrx +7 -7
- package/src/Navigate.tsrx +2 -1
- package/src/Outlet.tsrx +8 -6
- package/src/RouteNotFound.tsrx +2 -2
- package/src/RouterProvider.tsrx +12 -2
- package/src/RouterProvider.tsrx.d.ts +9 -5
- package/src/SafeFragment.tsrx +4 -1
- package/src/ScriptOnce.tsrx +23 -0
- package/src/ScriptOnce.tsrx.d.ts +3 -0
- package/src/Scripts.tsrx +42 -0
- package/src/Scripts.tsrx.d.ts +3 -0
- package/src/Transitioner.tsrx +15 -13
- package/src/assetKeys.ts +11 -0
- package/src/context.ts +5 -1
- package/src/externalHydration.ts +77 -0
- package/src/fileRoute.ts +277 -0
- package/src/frameworkTypes.ts +42 -0
- package/src/generator-plugin.d.ts +20 -0
- package/src/generator-plugin.js +100 -0
- package/src/headContentUtils.ts +172 -0
- package/src/hooks.ts +151 -0
- package/src/index.ts +95 -3
- package/src/lazyRouteComponent.ts +2 -1
- package/src/link.ts +25 -4
- package/src/linkTypes.ts +96 -0
- package/src/not-found.tsrx +19 -6
- package/src/not-found.tsrx.d.ts +5 -2
- package/src/octane-compiler.d.ts +12 -0
- package/src/route.ts +473 -36
- package/src/routeHookTypes.ts +228 -0
- package/src/router.ts +31 -6
- package/src/scriptContentUtils.ts +64 -0
- package/src/scroll-restoration.tsrx +16 -0
- package/src/scroll-restoration.tsrx.d.ts +3 -0
- package/src/ssr/RouterClient.tsrx +36 -0
- package/src/ssr/RouterClient.tsrx.d.ts +4 -0
- package/src/ssr/RouterServer.tsrx +6 -0
- package/src/ssr/RouterServer.tsrx.d.ts +4 -0
- package/src/ssr/client.ts +4 -0
- package/src/ssr/defaultRenderHandler.ts +11 -0
- package/src/ssr/defaultStreamHandler.ts +12 -0
- package/src/ssr/renderRouterToStream.ts +195 -0
- package/src/ssr/renderRouterToString.ts +58 -0
- package/src/ssr/server.ts +8 -0
- package/src/structuralSharing.ts +41 -0
- package/src/typePrimitives.ts +77 -0
- package/src/useAwaited.ts +7 -2
- package/src/useBlocker.tsrx +73 -8
- package/src/useBlocker.tsrx.d.ts +58 -2
- package/src/useRouterState.ts +20 -0
- package/src/useStore.ts +17 -6
package/src/fileRoute.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
import { useRouter } from './context';
|
|
11
|
+
import { splitSlot, subSlot } from './internal';
|
|
12
|
+
import { useStore } from './useStore';
|
|
13
|
+
import type {
|
|
14
|
+
AnyRouteMatch,
|
|
15
|
+
AnyRouter,
|
|
16
|
+
AssetCrossOriginConfig,
|
|
17
|
+
RouterManagedTag,
|
|
18
|
+
} from '@tanstack/router-core';
|
|
19
|
+
|
|
20
|
+
function buildTagsFromMatches(
|
|
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
|
+
}
|