@alepha/react 0.6.3 → 0.6.4
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/dist/index.browser.cjs +2 -1
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.js +3 -2
- package/dist/index.browser.js.map +1 -0
- package/dist/index.cjs +3 -2
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -0
- package/dist/{useActive-BVqdq757.cjs → useActive-BGtt_RNQ.cjs} +7 -6
- package/dist/useActive-BGtt_RNQ.cjs.map +1 -0
- package/dist/{useActive-dAmCT31a.js → useActive-QkvcaSmu.js} +7 -6
- package/dist/useActive-QkvcaSmu.js.map +1 -0
- package/package.json +10 -6
- package/src/components/Link.tsx +35 -0
- package/src/components/NestedView.tsx +36 -0
- package/src/contexts/RouterContext.ts +18 -0
- package/src/contexts/RouterLayerContext.ts +10 -0
- package/src/descriptors/$page.ts +143 -0
- package/src/errors/RedirectionError.ts +7 -0
- package/src/hooks/RouterHookApi.ts +156 -0
- package/src/hooks/useActive.ts +57 -0
- package/src/hooks/useClient.ts +6 -0
- package/src/hooks/useInject.ts +14 -0
- package/src/hooks/useQueryParams.ts +59 -0
- package/src/hooks/useRouter.ts +25 -0
- package/src/hooks/useRouterEvents.ts +43 -0
- package/src/hooks/useRouterState.ts +23 -0
- package/src/index.browser.ts +21 -0
- package/src/index.shared.ts +15 -0
- package/src/index.ts +48 -0
- package/src/providers/BrowserHeadProvider.ts +43 -0
- package/src/providers/BrowserRouterProvider.ts +146 -0
- package/src/providers/PageDescriptorProvider.ts +534 -0
- package/src/providers/ReactBrowserProvider.ts +223 -0
- package/src/providers/ReactServerProvider.ts +278 -0
- package/src/providers/ServerHeadProvider.ts +91 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { $hook, $inject, $logger, Alepha, EventEmitter } from "@alepha/core";
|
|
2
|
+
import type { HttpClientLink } from "@alepha/server";
|
|
3
|
+
import { type ReactNode, createElement } from "react";
|
|
4
|
+
import NestedView from "../components/NestedView.tsx";
|
|
5
|
+
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
6
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
7
|
+
import {
|
|
8
|
+
$page,
|
|
9
|
+
type Head,
|
|
10
|
+
type PageDescriptorOptions,
|
|
11
|
+
} from "../descriptors/$page.ts";
|
|
12
|
+
import { RedirectionError } from "../errors/RedirectionError.ts";
|
|
13
|
+
|
|
14
|
+
export class PageDescriptorProvider {
|
|
15
|
+
protected readonly log = $logger();
|
|
16
|
+
protected readonly alepha = $inject(Alepha);
|
|
17
|
+
protected readonly pages: PageRoute[] = [];
|
|
18
|
+
|
|
19
|
+
public getPages(): PageRoute[] {
|
|
20
|
+
return this.pages;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public page(name: string): PageRoute {
|
|
24
|
+
for (const page of this.pages) {
|
|
25
|
+
if (page.name === name) {
|
|
26
|
+
return page;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(`Page ${name} not found`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public root(
|
|
34
|
+
state: RouterState,
|
|
35
|
+
context: PageReactContext = {},
|
|
36
|
+
events?: EventEmitter<RouterEvents>,
|
|
37
|
+
): ReactNode {
|
|
38
|
+
return createElement(
|
|
39
|
+
RouterContext.Provider,
|
|
40
|
+
{
|
|
41
|
+
value: {
|
|
42
|
+
alepha: this.alepha,
|
|
43
|
+
state,
|
|
44
|
+
context,
|
|
45
|
+
events: events ?? new EventEmitter<RouterEvents>(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
createElement(NestedView, {}, state.layers[0]?.element),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async createLayers(
|
|
53
|
+
route: PageRoute,
|
|
54
|
+
request: PageRequest,
|
|
55
|
+
): Promise<CreateLayersResult> {
|
|
56
|
+
const { pathname, search } = request.url;
|
|
57
|
+
const layers: Layer[] = []; // result layers
|
|
58
|
+
let context: Record<string, any> = {}; // all props
|
|
59
|
+
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
60
|
+
|
|
61
|
+
let parent = route.parent;
|
|
62
|
+
while (parent) {
|
|
63
|
+
stack.unshift({ route: parent });
|
|
64
|
+
parent = parent.parent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let forceRefresh = false;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < stack.length; i++) {
|
|
70
|
+
const it = stack[i];
|
|
71
|
+
const route = it.route;
|
|
72
|
+
const config: Record<string, any> = {};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
config.query = route.schema?.query
|
|
76
|
+
? this.alepha.parse(route.schema.query, request.query)
|
|
77
|
+
: request.query;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
it.error = e as Error;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
config.params = route.schema?.params
|
|
85
|
+
? this.alepha.parse(route.schema.params, request.params)
|
|
86
|
+
: request.params;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
it.error = e as Error;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// save config
|
|
93
|
+
it.config = {
|
|
94
|
+
...config,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// no resolve, render a basic view by default
|
|
98
|
+
if (!route.resolve) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// check if previous layer is the same, reuse if possible
|
|
103
|
+
const previous = request.previous;
|
|
104
|
+
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
105
|
+
const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
|
|
106
|
+
|
|
107
|
+
const prev = JSON.stringify({
|
|
108
|
+
part: url(previous[i].part),
|
|
109
|
+
params: previous[i].config?.params ?? {},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const curr = JSON.stringify({
|
|
113
|
+
part: url(route.path),
|
|
114
|
+
params: config.params ?? {},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (prev === curr) {
|
|
118
|
+
// part is the same, reuse previous layer
|
|
119
|
+
it.props = previous[i].props;
|
|
120
|
+
it.error = previous[i].error;
|
|
121
|
+
context = {
|
|
122
|
+
...context,
|
|
123
|
+
...it.props,
|
|
124
|
+
};
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// part is different, force refresh of next layers
|
|
128
|
+
forceRefresh = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const props =
|
|
133
|
+
(await route.resolve?.({
|
|
134
|
+
...request, // request
|
|
135
|
+
...config, // params, query
|
|
136
|
+
...context, // previous props
|
|
137
|
+
} as any)) ?? {};
|
|
138
|
+
|
|
139
|
+
// save props
|
|
140
|
+
it.props = {
|
|
141
|
+
...props,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// add props to context
|
|
145
|
+
context = {
|
|
146
|
+
...context,
|
|
147
|
+
...props,
|
|
148
|
+
};
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// check if we need to redirect
|
|
151
|
+
if (e instanceof RedirectionError) {
|
|
152
|
+
return {
|
|
153
|
+
layers: [],
|
|
154
|
+
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
155
|
+
head: request.head,
|
|
156
|
+
pathname,
|
|
157
|
+
search,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.log.error(e);
|
|
162
|
+
|
|
163
|
+
it.error = e as Error;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let acc = "";
|
|
169
|
+
for (let i = 0; i < stack.length; i++) {
|
|
170
|
+
const it = stack[i];
|
|
171
|
+
const props = it.props ?? {};
|
|
172
|
+
|
|
173
|
+
const params = { ...it.config?.params };
|
|
174
|
+
for (const key of Object.keys(params)) {
|
|
175
|
+
params[key] = String(params[key]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (it.route.head && !it.error) {
|
|
179
|
+
this.fillHead(it.route, request, {
|
|
180
|
+
...props,
|
|
181
|
+
...context,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
acc += "/";
|
|
186
|
+
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
187
|
+
const path = acc.replace(/\/+/, "/");
|
|
188
|
+
|
|
189
|
+
// handler has thrown an error, render an error view
|
|
190
|
+
if (it.error) {
|
|
191
|
+
const errorHandler = this.getErrorHandler(it.route);
|
|
192
|
+
const element = await (errorHandler
|
|
193
|
+
? errorHandler({
|
|
194
|
+
...it.config,
|
|
195
|
+
error: it.error,
|
|
196
|
+
url: "",
|
|
197
|
+
})
|
|
198
|
+
: this.renderError(it.error));
|
|
199
|
+
|
|
200
|
+
layers.push({
|
|
201
|
+
props,
|
|
202
|
+
error: it.error,
|
|
203
|
+
name: it.route.name,
|
|
204
|
+
part: it.route.path,
|
|
205
|
+
config: it.config,
|
|
206
|
+
element: this.renderView(i + 1, path, element),
|
|
207
|
+
index: i + 1,
|
|
208
|
+
path,
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// normal use case
|
|
214
|
+
|
|
215
|
+
const layer = await this.createElement(it.route, {
|
|
216
|
+
...props,
|
|
217
|
+
...context,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
layers.push({
|
|
221
|
+
name: it.route.name,
|
|
222
|
+
props,
|
|
223
|
+
part: it.route.path,
|
|
224
|
+
config: it.config,
|
|
225
|
+
element: this.renderView(i + 1, path, layer),
|
|
226
|
+
index: i + 1,
|
|
227
|
+
path,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { layers, head: request.head, pathname, search };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
protected getErrorHandler(route: PageRoute) {
|
|
235
|
+
if (route.errorHandler) return route.errorHandler;
|
|
236
|
+
let parent = route.parent;
|
|
237
|
+
while (parent) {
|
|
238
|
+
if (parent.errorHandler) return parent.errorHandler;
|
|
239
|
+
parent = parent.parent;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected async createElement(
|
|
244
|
+
page: PageRoute,
|
|
245
|
+
props: Record<string, any>,
|
|
246
|
+
): Promise<ReactNode> {
|
|
247
|
+
if (page.lazy) {
|
|
248
|
+
const component = await page.lazy(); // load component
|
|
249
|
+
return createElement(component.default, props);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (page.component) {
|
|
253
|
+
return createElement(page.component, props);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected fillHead(
|
|
260
|
+
page: PageRoute,
|
|
261
|
+
ctx: PageRequest,
|
|
262
|
+
props: Record<string, any>,
|
|
263
|
+
): void {
|
|
264
|
+
if (!page.head) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
ctx.head ??= {};
|
|
269
|
+
|
|
270
|
+
const head =
|
|
271
|
+
typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
|
|
272
|
+
|
|
273
|
+
if (head.title) {
|
|
274
|
+
ctx.head ??= {};
|
|
275
|
+
|
|
276
|
+
if (ctx.head.titleSeparator) {
|
|
277
|
+
ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
|
|
278
|
+
} else {
|
|
279
|
+
ctx.head.title = head.title;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
ctx.head.titleSeparator = head.titleSeparator;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (head.htmlAttributes) {
|
|
286
|
+
ctx.head.htmlAttributes = {
|
|
287
|
+
...ctx.head.htmlAttributes,
|
|
288
|
+
...head.htmlAttributes,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (head.bodyAttributes) {
|
|
293
|
+
ctx.head.bodyAttributes = {
|
|
294
|
+
...ctx.head.bodyAttributes,
|
|
295
|
+
...head.bodyAttributes,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (head.meta) {
|
|
300
|
+
ctx.head.meta = [...(ctx.head.meta ?? []), ...(head.meta ?? [])];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public renderError(e: Error): ReactNode {
|
|
305
|
+
return createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public renderEmptyView(): ReactNode {
|
|
309
|
+
return createElement(NestedView, {});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public href(
|
|
313
|
+
page: { options: { name?: string } },
|
|
314
|
+
params: Record<string, any> = {},
|
|
315
|
+
): string {
|
|
316
|
+
const found = this.pages.find((it) => it.name === page.options.name);
|
|
317
|
+
if (!found) {
|
|
318
|
+
throw new Error(`Page ${page.options.name} not found`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let url = found.path ?? "";
|
|
322
|
+
let parent = found.parent;
|
|
323
|
+
while (parent) {
|
|
324
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
325
|
+
parent = parent.parent;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
url = this.compile(url, params);
|
|
329
|
+
|
|
330
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public compile(path: string, params: Record<string, string> = {}) {
|
|
334
|
+
for (const [key, value] of Object.entries(params)) {
|
|
335
|
+
path = path.replace(`:${key}`, value);
|
|
336
|
+
}
|
|
337
|
+
return path;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
protected renderView(
|
|
341
|
+
index: number,
|
|
342
|
+
path: string,
|
|
343
|
+
view: ReactNode = this.renderEmptyView(),
|
|
344
|
+
): ReactNode {
|
|
345
|
+
return createElement(
|
|
346
|
+
RouterLayerContext.Provider,
|
|
347
|
+
{
|
|
348
|
+
value: {
|
|
349
|
+
index,
|
|
350
|
+
path,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
view,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected readonly configure = $hook({
|
|
358
|
+
name: "configure",
|
|
359
|
+
handler: () => {
|
|
360
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
361
|
+
for (const { value, key } of pages) {
|
|
362
|
+
value.options.name ??= key;
|
|
363
|
+
|
|
364
|
+
// skip children, we only want root pages
|
|
365
|
+
if (value.options.parent) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.add(this.map(pages, value));
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
protected map(
|
|
375
|
+
pages: Array<{ value: { options: PageDescriptorOptions } }>,
|
|
376
|
+
target: { options: PageDescriptorOptions },
|
|
377
|
+
): PageRouteEntry {
|
|
378
|
+
const children = target.options.children ?? [];
|
|
379
|
+
|
|
380
|
+
for (const it of pages) {
|
|
381
|
+
if (it.value.options.parent === target) {
|
|
382
|
+
children.push(it.value);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
...target.options,
|
|
388
|
+
parent: undefined,
|
|
389
|
+
children: children.map((it) => this.map(pages, it)),
|
|
390
|
+
} as PageRoute;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public add(entry: PageRouteEntry) {
|
|
394
|
+
if (this.alepha.isReady()) {
|
|
395
|
+
throw new Error("Router is already initialized");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
entry.name ??= this.nextId();
|
|
399
|
+
const page = entry as PageRoute;
|
|
400
|
+
|
|
401
|
+
page.match = this.createMatch(page);
|
|
402
|
+
this.pages.push(page);
|
|
403
|
+
|
|
404
|
+
if (page.children) {
|
|
405
|
+
for (const child of page.children) {
|
|
406
|
+
(child as PageRoute).parent = page;
|
|
407
|
+
this.add(child);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
protected createMatch(page: PageRoute): string {
|
|
413
|
+
let url = page.path ?? "/";
|
|
414
|
+
let target = page.parent;
|
|
415
|
+
while (target) {
|
|
416
|
+
url = `${target.path ?? ""}/${url}`;
|
|
417
|
+
target = target.parent;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let path = url.replace(/\/\/+/g, "/");
|
|
421
|
+
|
|
422
|
+
if (path.endsWith("/") && path !== "/") {
|
|
423
|
+
// remove trailing slash
|
|
424
|
+
path = path.slice(0, -1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return path;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
protected _next = 0;
|
|
431
|
+
|
|
432
|
+
protected nextId(): string {
|
|
433
|
+
this._next += 1;
|
|
434
|
+
return `P${this._next}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const isPageRoute = (it: any): it is PageRoute => {
|
|
439
|
+
return (
|
|
440
|
+
it &&
|
|
441
|
+
typeof it === "object" &&
|
|
442
|
+
typeof it.path === "string" &&
|
|
443
|
+
typeof it.page === "object"
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export interface PageRouteEntry
|
|
448
|
+
extends Omit<PageDescriptorOptions, "children" | "parent"> {
|
|
449
|
+
children?: PageRouteEntry[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export interface PageRoute extends PageRouteEntry {
|
|
453
|
+
type: "page";
|
|
454
|
+
name: string;
|
|
455
|
+
parent?: PageRoute;
|
|
456
|
+
match: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface Layer {
|
|
460
|
+
config?: {
|
|
461
|
+
query?: Record<string, any>;
|
|
462
|
+
params?: Record<string, any>;
|
|
463
|
+
// stack of resolved props
|
|
464
|
+
context?: Record<string, any>;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
name: string;
|
|
468
|
+
props?: Record<string, any>;
|
|
469
|
+
error?: Error;
|
|
470
|
+
part?: string;
|
|
471
|
+
element: ReactNode;
|
|
472
|
+
index: number;
|
|
473
|
+
path: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export type PreviousLayerData = Omit<Layer, "element">;
|
|
477
|
+
|
|
478
|
+
export interface AnchorProps {
|
|
479
|
+
href?: string;
|
|
480
|
+
onClick?: (ev: any) => any;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export interface RouterEvents {
|
|
484
|
+
begin: undefined;
|
|
485
|
+
success: undefined;
|
|
486
|
+
error: Error;
|
|
487
|
+
end: RouterState;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export interface RouterState {
|
|
491
|
+
pathname: string;
|
|
492
|
+
search: string;
|
|
493
|
+
layers: Array<Layer>;
|
|
494
|
+
head: Head;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface TransitionOptions {
|
|
498
|
+
state?: RouterState;
|
|
499
|
+
previous?: PreviousLayerData[];
|
|
500
|
+
context?: PageReactContext;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export interface RouterStackItem {
|
|
504
|
+
route: PageRoute;
|
|
505
|
+
config?: Record<string, any>;
|
|
506
|
+
props?: Record<string, any>;
|
|
507
|
+
error?: Error;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export interface RouterRenderResult {
|
|
511
|
+
redirect?: string;
|
|
512
|
+
layers: Layer[];
|
|
513
|
+
head: Head;
|
|
514
|
+
element: ReactNode;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export interface PageRequest extends PageReactContext {
|
|
518
|
+
url: URL;
|
|
519
|
+
params: Record<string, any>;
|
|
520
|
+
query: Record<string, string>;
|
|
521
|
+
head: Head;
|
|
522
|
+
|
|
523
|
+
// previous layers (browser history or browser hydration, always null on server)
|
|
524
|
+
previous?: PreviousLayerData[];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface CreateLayersResult extends RouterState {
|
|
528
|
+
redirect?: string;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// will be passed to ReactContext
|
|
532
|
+
export interface PageReactContext {
|
|
533
|
+
links?: HttpClientLink[];
|
|
534
|
+
}
|