@uniflowed/router 0.0.0-alpha.32 → 0.0.0-alpha.34
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/index.js +7 -0
- package/internal/boundaries.js +11 -110
- package/internal/boundary-data.js +88 -0
- package/internal/inspector.js +12 -1
- package/internal/resolved-summary.js +199 -0
- package/internal/routing.js +56 -8
- package/internal/runtime.js +529 -46
- package/native.js +408 -0
- package/package.json +5 -3
- package/routing.js +9 -0
- package/server.js +7 -7
package/index.js
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
// URL the page is, so one URL renders two subtrees at once, and
|
|
23
23
|
// `$default.js` is what a slot renders when the URL matched none of its
|
|
24
24
|
// routes. See ubugeeei-prod/uf#267.
|
|
25
|
+
//
|
|
26
|
+
// A directory named `(.)photo` inside a slot is an intercepting route: a client
|
|
27
|
+
// navigation that starts on a page the slot is on, and reaches the URL the
|
|
28
|
+
// directory stands in for, renders it in the slot and leaves the page
|
|
29
|
+
// underneath where it was. A document request for that URL — a reload, a
|
|
30
|
+
// shared link, a prerender — renders the ordinary page.
|
|
25
31
|
|
|
26
32
|
import * as React from "react";
|
|
27
33
|
|
|
@@ -31,6 +37,7 @@ export type {
|
|
|
31
37
|
AppProps,
|
|
32
38
|
ErrorBoundary,
|
|
33
39
|
ErrorModule,
|
|
40
|
+
Interception,
|
|
34
41
|
JsonLd,
|
|
35
42
|
LayoutModule,
|
|
36
43
|
LinkPrefetch,
|
package/internal/boundaries.js
CHANGED
|
@@ -86,7 +86,18 @@
|
|
|
86
86
|
import * as React from "react";
|
|
87
87
|
import { useEffect, useState } from "react";
|
|
88
88
|
|
|
89
|
+
import { SYNTHESISED_SOURCE } from "./boundary-data.js";
|
|
89
90
|
import { reportDiagnostic } from "./diagnostics.js";
|
|
91
|
+
import type { RouteBoundary } from "./boundary-data.js";
|
|
92
|
+
|
|
93
|
+
export type { BoundaryKind, RouteBoundary } from "./boundary-data.js";
|
|
94
|
+
export {
|
|
95
|
+
ROOT_ERROR_ID,
|
|
96
|
+
ROUTE_ERROR_ID,
|
|
97
|
+
SYNTHESISED_SOURCE,
|
|
98
|
+
routeBoundaries,
|
|
99
|
+
suspenseId,
|
|
100
|
+
} from "./boundary-data.js";
|
|
90
101
|
|
|
91
102
|
/** The attribute a mark carries its boundary's id in. */
|
|
92
103
|
export const BOUNDARY_ATTRIBUTE: string = "data-uf-boundary";
|
|
@@ -100,116 +111,6 @@ export const SOURCE_ATTRIBUTE: string = "data-uf-boundary-source";
|
|
|
100
111
|
/** The name `uf dev` installs the on-demand report under. */
|
|
101
112
|
export const BOUNDARY_GLOBAL: string = "__ufBoundaries";
|
|
102
113
|
|
|
103
|
-
/**
|
|
104
|
-
* What `file` says for the error boundary the build synthesises.
|
|
105
|
-
*
|
|
106
|
-
* `routesModuleSource` writes this string where a declared boundary has a path,
|
|
107
|
-
* because the record has no module to name — the framework's own error page
|
|
108
|
-
* renders in its place. Written out again rather than imported for the reason
|
|
109
|
-
* `./devtools.js` gives about `DEVTOOLS_HOOK`: `@uniflowed/vite` is plain
|
|
110
|
-
* JavaScript loaded by Vite before any Flow transform exists, so the import
|
|
111
|
-
* cannot go either way. `boundaries.test.js` holds the two spellings together.
|
|
112
|
-
*/
|
|
113
|
-
export const SYNTHESISED_SOURCE: string = "@uniflowed/router";
|
|
114
|
-
|
|
115
|
-
/** Which kind of boundary a mark belongs to. */
|
|
116
|
-
export type BoundaryKind = "suspense" | "error";
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* One boundary of a resolved route, as the marks and the report name it.
|
|
120
|
-
*
|
|
121
|
-
* `id` pairs the two marks and is stable for a boundary across renders, so a
|
|
122
|
-
* navigation that keeps a boundary keeps its marks mounted. `above` is how many
|
|
123
|
-
* of the route's layouts are outside it — the same number, spelled the same
|
|
124
|
-
* way, that `ResolvedRoute["errorBoundary"].above` and every `loading` entry
|
|
125
|
-
* carry, because there is no second vocabulary for where a thing sits in the
|
|
126
|
-
* stack.
|
|
127
|
-
*
|
|
128
|
-
* `source` is the file it was declared in, and is `null` for every `<Suspense>`
|
|
129
|
-
* boundary. That asymmetry is the route table's rather than this module's: an
|
|
130
|
-
* error boundary is matched by path, so the table carries its `file`, while a
|
|
131
|
-
* `$loading.js` is carried by depth alone. Adding a path to the loading
|
|
132
|
-
* records would put one in every visitor's bundle to serve a report only
|
|
133
|
-
* `uf dev` reads.
|
|
134
|
-
*/
|
|
135
|
-
export type RouteBoundary = {|
|
|
136
|
-
readonly id: string,
|
|
137
|
-
readonly kind: BoundaryKind,
|
|
138
|
-
readonly above: number,
|
|
139
|
-
readonly source: ?string,
|
|
140
|
-
|};
|
|
141
|
-
|
|
142
|
-
/** The id of the `<Suspense>` boundary at `index` of a route's `loading`. */
|
|
143
|
-
export function suspenseId(index: number): string {
|
|
144
|
-
return `suspense:${index}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** The id of the boundary a route's own `$error.js` renders. */
|
|
148
|
-
export const ROUTE_ERROR_ID: string = "error:route";
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* The id of the boundary that stands outside every layout.
|
|
152
|
-
*
|
|
153
|
-
* It has no module and renders the framework's page; it is what is between a
|
|
154
|
-
* throw in a root layout, or in the error component itself, and an unmounted
|
|
155
|
-
* document. Marked like any other, because "which subtree does the last resort
|
|
156
|
-
* own" is exactly as unanswerable from the page as the rest.
|
|
157
|
-
*/
|
|
158
|
-
export const ROOT_ERROR_ID: string = "error:root";
|
|
159
|
-
|
|
160
|
-
/** The part of a resolved route this module reads. */
|
|
161
|
-
type BoundedRoute = {
|
|
162
|
-
readonly errorBoundary: { readonly above: number, ... },
|
|
163
|
-
readonly loading: $ReadOnlyArray<{ readonly above: number, ... }>,
|
|
164
|
-
readonly error: mixed,
|
|
165
|
-
...
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Every boundary a resolved route renders, in the order they nest.
|
|
170
|
-
*
|
|
171
|
-
* A `Map` rather than a list because it is read both ways: `RouteView` asks for
|
|
172
|
-
* one by id as it builds the stack, and the report walks the values. One
|
|
173
|
-
* function answering both is the point — an id `RouteView` marks and the report
|
|
174
|
-
* cannot find is a boundary that silently disappears from the map, and
|
|
175
|
-
* ubugeeei-prod/uf#636 made the same argument about an explanation that can
|
|
176
|
-
* disagree with the thing it explains.
|
|
177
|
-
*
|
|
178
|
-
* The route's own error boundary is absent when the route *is* its error page,
|
|
179
|
-
* which is exactly when `RouteView` does not render one: wrapping that page in
|
|
180
|
-
* the boundary whose component it is would answer a throw inside it with
|
|
181
|
-
* itself.
|
|
182
|
-
*
|
|
183
|
-
* @param resolved the route being rendered
|
|
184
|
-
* @param errorSource the `file` of the nearest `$error.js`, when the table
|
|
185
|
-
* has one; `null` leaves the boundary named by its depth alone
|
|
186
|
-
*/
|
|
187
|
-
export function routeBoundaries(
|
|
188
|
-
resolved: BoundedRoute,
|
|
189
|
-
errorSource: ?string,
|
|
190
|
-
): Map<string, RouteBoundary> {
|
|
191
|
-
const found: Map<string, RouteBoundary> = new Map();
|
|
192
|
-
found.set(ROOT_ERROR_ID, {
|
|
193
|
-
id: ROOT_ERROR_ID,
|
|
194
|
-
kind: "error",
|
|
195
|
-
above: 0,
|
|
196
|
-
source: SYNTHESISED_SOURCE,
|
|
197
|
-
});
|
|
198
|
-
if (resolved.error == null) {
|
|
199
|
-
found.set(ROUTE_ERROR_ID, {
|
|
200
|
-
id: ROUTE_ERROR_ID,
|
|
201
|
-
kind: "error",
|
|
202
|
-
above: resolved.errorBoundary.above,
|
|
203
|
-
source: errorSource,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
resolved.loading.forEach((boundary, index) => {
|
|
207
|
-
const id = suspenseId(index);
|
|
208
|
-
found.set(id, { id, kind: "suspense", above: boundary.above, source: null });
|
|
209
|
-
});
|
|
210
|
-
return found;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
114
|
/**
|
|
214
115
|
* Whether an edge that mounts now should be in the DOM immediately.
|
|
215
116
|
*
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// React-free names for the boundaries a resolved route renders.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What `file` says for the error boundary the build synthesises.
|
|
7
|
+
*
|
|
8
|
+
* `routesModuleSource` writes this string where a declared boundary has a path,
|
|
9
|
+
* because the record has no module to name: the framework's own error page
|
|
10
|
+
* renders in its place. This module is plain data so the routing surface and
|
|
11
|
+
* the dev-only DOM marker code can share one spelling without importing React.
|
|
12
|
+
*/
|
|
13
|
+
export const SYNTHESISED_SOURCE: string = "@uniflowed/router";
|
|
14
|
+
|
|
15
|
+
/** Which kind of boundary a mark or payload row belongs to. */
|
|
16
|
+
export type BoundaryKind = "suspense" | "error";
|
|
17
|
+
|
|
18
|
+
/** The id of the `<Suspense>` boundary at `index` of a route's `loading`. */
|
|
19
|
+
export function suspenseId(index: number): string {
|
|
20
|
+
return `suspense:${index}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The id of the boundary a route's own `$error.js` renders. */
|
|
24
|
+
export const ROUTE_ERROR_ID: string = "error:route";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The id of the boundary that stands outside every layout.
|
|
28
|
+
*
|
|
29
|
+
* It has no module and renders the framework's page; it is what is between a
|
|
30
|
+
* throw in a root layout, or in the error component itself, and an unmounted
|
|
31
|
+
* document.
|
|
32
|
+
*/
|
|
33
|
+
export const ROOT_ERROR_ID: string = "error:root";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One boundary of a resolved route, in uf's vocabulary rather than React's.
|
|
37
|
+
*
|
|
38
|
+
* `id` is the stable name `RouteView` marks in the DOM today and the name a
|
|
39
|
+
* future element payload can put beside the chunk that completed it. `above`
|
|
40
|
+
* is how many layouts are outside the boundary.
|
|
41
|
+
*/
|
|
42
|
+
export type RouteBoundary = {|
|
|
43
|
+
readonly id: string,
|
|
44
|
+
readonly kind: BoundaryKind,
|
|
45
|
+
readonly above: number,
|
|
46
|
+
readonly source: ?string,
|
|
47
|
+
|};
|
|
48
|
+
|
|
49
|
+
/** The part of a resolved route the boundary map reads. */
|
|
50
|
+
type BoundedRoute = {
|
|
51
|
+
readonly errorBoundary: { readonly above: number, ... },
|
|
52
|
+
readonly loading: $ReadOnlyArray<{ readonly above: number, ... }>,
|
|
53
|
+
readonly error: mixed,
|
|
54
|
+
...
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every boundary a resolved route renders, in the order they nest.
|
|
59
|
+
*
|
|
60
|
+
* A `Map` rather than a list because callers read it both ways: `RouteView`
|
|
61
|
+
* asks for one by id as it builds the stack, while diagnostics and payload
|
|
62
|
+
* summaries walk the values.
|
|
63
|
+
*/
|
|
64
|
+
export function routeBoundaries(
|
|
65
|
+
resolved: BoundedRoute,
|
|
66
|
+
errorSource: ?string,
|
|
67
|
+
): Map<string, RouteBoundary> {
|
|
68
|
+
const found: Map<string, RouteBoundary> = new Map();
|
|
69
|
+
found.set(ROOT_ERROR_ID, {
|
|
70
|
+
id: ROOT_ERROR_ID,
|
|
71
|
+
kind: "error",
|
|
72
|
+
above: 0,
|
|
73
|
+
source: SYNTHESISED_SOURCE,
|
|
74
|
+
});
|
|
75
|
+
if (resolved.error == null) {
|
|
76
|
+
found.set(ROUTE_ERROR_ID, {
|
|
77
|
+
id: ROUTE_ERROR_ID,
|
|
78
|
+
kind: "error",
|
|
79
|
+
above: resolved.errorBoundary.above,
|
|
80
|
+
source: errorSource,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
resolved.loading.forEach((boundary, index) => {
|
|
84
|
+
const id = suspenseId(index);
|
|
85
|
+
found.set(id, { id, kind: "suspense", above: boundary.above, source: null });
|
|
86
|
+
});
|
|
87
|
+
return found;
|
|
88
|
+
}
|
package/internal/inspector.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// lands this is the recorder it is fed to: `inspectStream` takes chunks and
|
|
28
28
|
// knows nothing about who produced them.
|
|
29
29
|
//
|
|
30
|
-
// # It reads React's
|
|
30
|
+
// # It reads React's markers, plus uf's row marker
|
|
31
31
|
//
|
|
32
32
|
// Fizz writes a suspended boundary into the shell as an empty
|
|
33
33
|
// `<template id="B:0">` followed by the fallback and closed by a `<!--/$-->`
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
// content of these chunks is the application's own — with a step limit on every
|
|
47
47
|
// loop for the same reason `./boundaries.js` has a `WALK_LIMIT`.
|
|
48
48
|
//
|
|
49
|
+
// The one uf-owned marker read here is `data-uf-row` on deferred payload row
|
|
50
|
+
// scripts. The inspector does not parse the row JSON or attach semantics to
|
|
51
|
+
// the value; it only keeps the terminal report from saying a row boundary built
|
|
52
|
+
// an anonymous `script`.
|
|
53
|
+
//
|
|
49
54
|
// A marker that a chunk boundary happened to split is not attributed rather
|
|
50
55
|
// than guessed at: the chunk is still counted, still timed, and says so. The
|
|
51
56
|
// alternative is a report that is confidently wrong about which part of the
|
|
@@ -81,6 +86,8 @@
|
|
|
81
86
|
// honest report is silence. That is also what keeps this from being a request
|
|
82
87
|
// log: it fires for the pages the feature is about and for no others.
|
|
83
88
|
|
|
89
|
+
import { PAYLOAD_ROW_ATTRIBUTE } from "./payload.js";
|
|
90
|
+
|
|
84
91
|
/** How many chunks of one document are recorded before the rest are counted. */
|
|
85
92
|
const CHUNK_LIMIT = 64;
|
|
86
93
|
|
|
@@ -412,6 +419,10 @@ function topLevelElements(markup: string, from: number): $ReadOnlyArray<string>
|
|
|
412
419
|
export function describeTag(tag: string): string {
|
|
413
420
|
const space = tag.search(/[\s/>]/);
|
|
414
421
|
const name = tag.slice(1, space === -1 ? tag.length : space).toLowerCase();
|
|
422
|
+
const row = name === "script" ? attribute(tag, PAYLOAD_ROW_ATTRIBUTE) : null;
|
|
423
|
+
if (row != null && row !== "") {
|
|
424
|
+
return `deferred payload row ${row}`;
|
|
425
|
+
}
|
|
415
426
|
const id = attribute(tag, "id");
|
|
416
427
|
if (id != null && id !== "") {
|
|
417
428
|
return `${name}#${id}`;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Module-free summaries of resolved routes.
|
|
4
|
+
|
|
5
|
+
import { routeBoundaries, suspenseId } from "./boundary-data.js";
|
|
6
|
+
import type { RouteBoundary } from "./boundary-data.js";
|
|
7
|
+
import type { RouteParams, SearchParams } from "./routing.js";
|
|
8
|
+
|
|
9
|
+
type RouteStatus = 200 | 401 | 403 | 404 | 500;
|
|
10
|
+
|
|
11
|
+
type RouteMetadata = { readonly [string]: mixed, ... };
|
|
12
|
+
|
|
13
|
+
type RouteErrorLike = {
|
|
14
|
+
readonly kind: "thrown" | "unauthorized" | "forbidden",
|
|
15
|
+
...
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type BoundaryLike = {
|
|
19
|
+
readonly above: number,
|
|
20
|
+
...
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ModuleBoundaryLike = {
|
|
24
|
+
readonly above: number,
|
|
25
|
+
readonly module?: mixed,
|
|
26
|
+
...
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SlotLike = {
|
|
30
|
+
readonly name: string,
|
|
31
|
+
readonly above: number,
|
|
32
|
+
readonly page: mixed,
|
|
33
|
+
readonly params: RouteParams,
|
|
34
|
+
readonly layouts: $ReadOnlyArray<mixed>,
|
|
35
|
+
readonly loading: $ReadOnlyArray<BoundaryLike>,
|
|
36
|
+
readonly templates: $ReadOnlyArray<BoundaryLike>,
|
|
37
|
+
readonly errorBoundary: ?ModuleBoundaryLike,
|
|
38
|
+
readonly slots: $ReadOnlyArray<SlotLike>,
|
|
39
|
+
...
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RouteLike = {
|
|
43
|
+
readonly pathname: string,
|
|
44
|
+
readonly search: string,
|
|
45
|
+
readonly path: string,
|
|
46
|
+
readonly params: RouteParams,
|
|
47
|
+
readonly searchParams: SearchParams,
|
|
48
|
+
readonly layouts: $ReadOnlyArray<mixed>,
|
|
49
|
+
readonly data: mixed,
|
|
50
|
+
readonly deferred: mixed,
|
|
51
|
+
readonly metadata: RouteMetadata,
|
|
52
|
+
readonly viewTransition: ?string,
|
|
53
|
+
readonly status: RouteStatus,
|
|
54
|
+
readonly error: ?RouteErrorLike,
|
|
55
|
+
readonly errorBoundary: ModuleBoundaryLike,
|
|
56
|
+
readonly loading: $ReadOnlyArray<BoundaryLike>,
|
|
57
|
+
readonly templates: $ReadOnlyArray<BoundaryLike>,
|
|
58
|
+
readonly slots: $ReadOnlyArray<SlotLike>,
|
|
59
|
+
...
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ResolvedBoundarySummary = {|
|
|
63
|
+
readonly id: string,
|
|
64
|
+
readonly above: number,
|
|
65
|
+
|};
|
|
66
|
+
|
|
67
|
+
export type ResolvedTemplateSummary = {|
|
|
68
|
+
readonly above: number,
|
|
69
|
+
|};
|
|
70
|
+
|
|
71
|
+
export type ResolvedRouteErrorSummary =
|
|
72
|
+
| {| readonly kind: "thrown" |}
|
|
73
|
+
| {| readonly kind: "unauthorized" |}
|
|
74
|
+
| {| readonly kind: "forbidden" |};
|
|
75
|
+
|
|
76
|
+
export type ResolvedErrorBoundarySummary = {|
|
|
77
|
+
readonly above: number,
|
|
78
|
+
readonly custom: boolean,
|
|
79
|
+
readonly rendered: boolean,
|
|
80
|
+
|};
|
|
81
|
+
|
|
82
|
+
export type ResolvedSlotSummary = {|
|
|
83
|
+
readonly name: string,
|
|
84
|
+
readonly above: number,
|
|
85
|
+
readonly active: boolean,
|
|
86
|
+
readonly params: RouteParams,
|
|
87
|
+
readonly layoutCount: number,
|
|
88
|
+
readonly loading: $ReadOnlyArray<ResolvedBoundarySummary>,
|
|
89
|
+
readonly templates: $ReadOnlyArray<ResolvedTemplateSummary>,
|
|
90
|
+
readonly errorBoundary: ?ResolvedErrorBoundarySummary,
|
|
91
|
+
readonly slots: $ReadOnlyArray<ResolvedSlotSummary>,
|
|
92
|
+
|};
|
|
93
|
+
|
|
94
|
+
export type ResolvedRouteSummary = {|
|
|
95
|
+
readonly pathname: string,
|
|
96
|
+
readonly search: string,
|
|
97
|
+
readonly path: string,
|
|
98
|
+
readonly params: RouteParams,
|
|
99
|
+
readonly searchParams: SearchParams,
|
|
100
|
+
readonly layoutCount: number,
|
|
101
|
+
readonly data: mixed,
|
|
102
|
+
readonly deferred: boolean,
|
|
103
|
+
readonly metadata: RouteMetadata,
|
|
104
|
+
readonly viewTransition: ?string,
|
|
105
|
+
readonly status: RouteStatus,
|
|
106
|
+
readonly error: ?ResolvedRouteErrorSummary,
|
|
107
|
+
readonly errorBoundary: ResolvedErrorBoundarySummary,
|
|
108
|
+
readonly loading: $ReadOnlyArray<ResolvedBoundarySummary>,
|
|
109
|
+
readonly templates: $ReadOnlyArray<ResolvedTemplateSummary>,
|
|
110
|
+
readonly slots: $ReadOnlyArray<ResolvedSlotSummary>,
|
|
111
|
+
readonly boundaries: $ReadOnlyArray<RouteBoundary>,
|
|
112
|
+
|};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The part of a `ResolvedRoute` that can cross to a payload or diagnostic.
|
|
116
|
+
*
|
|
117
|
+
* A resolved route carries loaded modules so `RouteView` can render today.
|
|
118
|
+
* The element payload in ubugeeei-prod/uf#519 needs the other half: route
|
|
119
|
+
* identity, data, metadata, status, and boundary names, with page/layout/
|
|
120
|
+
* fallback modules left behind in the renderer graph.
|
|
121
|
+
*/
|
|
122
|
+
export function summarizeResolvedRoute(
|
|
123
|
+
route: RouteLike,
|
|
124
|
+
errorSource?: ?string,
|
|
125
|
+
): ResolvedRouteSummary {
|
|
126
|
+
return {
|
|
127
|
+
pathname: route.pathname,
|
|
128
|
+
search: route.search,
|
|
129
|
+
path: route.path,
|
|
130
|
+
params: route.params,
|
|
131
|
+
searchParams: route.searchParams,
|
|
132
|
+
layoutCount: route.layouts.length,
|
|
133
|
+
data: route.data,
|
|
134
|
+
deferred: route.deferred != null,
|
|
135
|
+
metadata: route.metadata,
|
|
136
|
+
viewTransition: route.viewTransition,
|
|
137
|
+
status: route.status,
|
|
138
|
+
error: summarizeError(route.error),
|
|
139
|
+
errorBoundary: summarizeErrorBoundary(route.errorBoundary, route.error == null),
|
|
140
|
+
loading: summarizeLoading(route.loading),
|
|
141
|
+
templates: summarizeTemplates(route.templates),
|
|
142
|
+
slots: route.slots.map(summarizeSlot),
|
|
143
|
+
boundaries: [...routeBoundaries(route, errorSource).values()],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function summarizeSlot(slot: SlotLike): ResolvedSlotSummary {
|
|
148
|
+
return {
|
|
149
|
+
name: slot.name,
|
|
150
|
+
above: slot.above,
|
|
151
|
+
active: slot.page != null,
|
|
152
|
+
params: slot.params,
|
|
153
|
+
layoutCount: slot.layouts.length,
|
|
154
|
+
loading: summarizeLoading(slot.loading),
|
|
155
|
+
templates: summarizeTemplates(slot.templates),
|
|
156
|
+
errorBoundary:
|
|
157
|
+
slot.errorBoundary == null ? null : summarizeErrorBoundary(slot.errorBoundary, true),
|
|
158
|
+
slots: slot.slots.map(summarizeSlot),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function summarizeLoading(
|
|
163
|
+
boundaries: $ReadOnlyArray<BoundaryLike>,
|
|
164
|
+
): $ReadOnlyArray<ResolvedBoundarySummary> {
|
|
165
|
+
return boundaries.map((boundary, index) => ({
|
|
166
|
+
id: suspenseId(index),
|
|
167
|
+
above: boundary.above,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function summarizeTemplates(
|
|
172
|
+
templates: $ReadOnlyArray<BoundaryLike>,
|
|
173
|
+
): $ReadOnlyArray<ResolvedTemplateSummary> {
|
|
174
|
+
return templates.map((template) => ({ above: template.above }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function summarizeErrorBoundary(
|
|
178
|
+
boundary: ModuleBoundaryLike,
|
|
179
|
+
rendered: boolean,
|
|
180
|
+
): ResolvedErrorBoundarySummary {
|
|
181
|
+
return {
|
|
182
|
+
above: boundary.above,
|
|
183
|
+
custom: boundary.module != null,
|
|
184
|
+
rendered,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarizeError(error: ?RouteErrorLike): ?ResolvedRouteErrorSummary {
|
|
189
|
+
if (error == null) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (error.kind === "unauthorized") {
|
|
193
|
+
return { kind: "unauthorized" };
|
|
194
|
+
}
|
|
195
|
+
if (error.kind === "forbidden") {
|
|
196
|
+
return { kind: "forbidden" };
|
|
197
|
+
}
|
|
198
|
+
return { kind: "thrown" };
|
|
199
|
+
}
|
package/internal/routing.js
CHANGED
|
@@ -31,29 +31,77 @@ export type LoadingRecord<TLoading = mixed> = {|
|
|
|
31
31
|
readonly module: RouteModule<TLoading>,
|
|
32
32
|
|};
|
|
33
33
|
|
|
34
|
+
/** One `$error.js` inside a slot, as the route table carries it. */
|
|
35
|
+
export type SlotErrorBoundaryRecord<TError = mixed> = {|
|
|
36
|
+
readonly above: number,
|
|
37
|
+
readonly module: RouteModule<TError>,
|
|
38
|
+
|};
|
|
39
|
+
|
|
34
40
|
/** One parallel-route slot, as the route table carries it. */
|
|
35
|
-
export type SlotRecord<
|
|
41
|
+
export type SlotRecord<
|
|
42
|
+
TPage = mixed,
|
|
43
|
+
TLayout = mixed,
|
|
44
|
+
TTemplate = mixed,
|
|
45
|
+
TLoading = mixed,
|
|
46
|
+
TError = mixed,
|
|
47
|
+
> = {|
|
|
36
48
|
readonly name: string,
|
|
37
49
|
readonly above: number,
|
|
38
50
|
readonly defaultPage: ?RouteModule<TPage>,
|
|
39
51
|
readonly defaultFile?: string,
|
|
40
52
|
readonly defaultMdx?: boolean,
|
|
41
|
-
readonly
|
|
53
|
+
readonly defaultErrorBoundary?: ?SlotErrorBoundaryRecord<TError>,
|
|
54
|
+
readonly routes: $ReadOnlyArray<SlotRouteRecord<TPage, TLayout, TTemplate, TLoading, TError>>,
|
|
55
|
+
/**
|
|
56
|
+
* The routes this slot renders when a client navigation *intercepts* the URL
|
|
57
|
+
* each one names — `app/feed/@modal/(.)photo/[id]/$page.js` is the one for
|
|
58
|
+
* `/feed/photo/:id` — keyed by that URL.
|
|
59
|
+
*
|
|
60
|
+
* A second list rather than a flag on `routes`, because the two are read by
|
|
61
|
+
* different callers at different times and neither may see the other's.
|
|
62
|
+
* `routes` is matched against every URL the segment renders, by the server and
|
|
63
|
+
* by the browser alike. These are matched only by a navigation that starts on
|
|
64
|
+
* a page this slot is already rendered on, and nothing on a server reads them
|
|
65
|
+
* — which is the whole of why a document request for an intercepted URL
|
|
66
|
+
* renders the ordinary page. `resolveInterception`, beside the rest of the
|
|
67
|
+
* rendering, is the one reader.
|
|
68
|
+
*
|
|
69
|
+
* Absent on a slot that intercepts nothing, which is every slot a table
|
|
70
|
+
* written before interception existed.
|
|
71
|
+
*/
|
|
72
|
+
readonly intercepts?: $ReadOnlyArray<
|
|
73
|
+
SlotRouteRecord<TPage, TLayout, TTemplate, TLoading, TError>,
|
|
74
|
+
>,
|
|
42
75
|
|};
|
|
43
76
|
|
|
44
77
|
/** One page inside a slot. */
|
|
45
|
-
export type SlotRouteRecord<
|
|
78
|
+
export type SlotRouteRecord<
|
|
79
|
+
TPage = mixed,
|
|
80
|
+
TLayout = mixed,
|
|
81
|
+
TTemplate = mixed,
|
|
82
|
+
TLoading = mixed,
|
|
83
|
+
TError = mixed,
|
|
84
|
+
> = {|
|
|
46
85
|
readonly path: string,
|
|
47
86
|
readonly params: $ReadOnlyArray<RouteParamSpec>,
|
|
48
87
|
readonly mdx: boolean,
|
|
49
88
|
readonly file: string,
|
|
50
89
|
readonly page: RouteModule<TPage>,
|
|
51
90
|
readonly layouts: $ReadOnlyArray<RouteModule<TLayout>>,
|
|
52
|
-
readonly
|
|
91
|
+
readonly loading?: $ReadOnlyArray<LoadingRecord<TLoading>>,
|
|
92
|
+
readonly templates?: $ReadOnlyArray<TemplateRecord<TTemplate>>,
|
|
93
|
+
readonly errorBoundary?: ?SlotErrorBoundaryRecord<TError>,
|
|
94
|
+
readonly slots: $ReadOnlyArray<SlotRecord<TPage, TLayout, TTemplate, TLoading, TError>>,
|
|
53
95
|
|};
|
|
54
96
|
|
|
55
97
|
/** One entry of the generated route table. */
|
|
56
|
-
export type RouteRecord<
|
|
98
|
+
export type RouteRecord<
|
|
99
|
+
TPage = mixed,
|
|
100
|
+
TLayout = mixed,
|
|
101
|
+
TTemplate = mixed,
|
|
102
|
+
TLoading = mixed,
|
|
103
|
+
TError = mixed,
|
|
104
|
+
> = {|
|
|
57
105
|
readonly path: string,
|
|
58
106
|
readonly params: $ReadOnlyArray<RouteParamSpec>,
|
|
59
107
|
readonly mdx: boolean,
|
|
@@ -62,7 +110,7 @@ export type RouteRecord<TPage = mixed, TLayout = mixed, TTemplate = mixed, TLoad
|
|
|
62
110
|
readonly layouts: $ReadOnlyArray<RouteModule<TLayout>>,
|
|
63
111
|
readonly loading?: $ReadOnlyArray<LoadingRecord<TLoading>>,
|
|
64
112
|
readonly templates?: $ReadOnlyArray<TemplateRecord<TTemplate>>,
|
|
65
|
-
readonly slots?: $ReadOnlyArray<SlotRecord<TPage, TLayout>>,
|
|
113
|
+
readonly slots?: $ReadOnlyArray<SlotRecord<TPage, TLayout, TTemplate, TLoading, TError>>,
|
|
66
114
|
|};
|
|
67
115
|
|
|
68
116
|
/** One not-found boundary: the page for a path under `path` that matched nothing. */
|
|
@@ -90,12 +138,12 @@ export type RouteTable<
|
|
|
90
138
|
TLoading = mixed,
|
|
91
139
|
TError = mixed,
|
|
92
140
|
> = {|
|
|
93
|
-
readonly routes: $ReadOnlyArray<RouteRecord<TPage, TLayout, TTemplate, TLoading>>,
|
|
141
|
+
readonly routes: $ReadOnlyArray<RouteRecord<TPage, TLayout, TTemplate, TLoading, TError>>,
|
|
94
142
|
readonly notFound: $ReadOnlyArray<NotFoundBoundary<TPage, TLayout>>,
|
|
95
143
|
readonly errors: $ReadOnlyArray<ErrorBoundary<TError, TLayout>>,
|
|
96
144
|
|};
|
|
97
145
|
|
|
98
|
-
type UnknownRouteRecord = RouteRecord<mixed, mixed, mixed, mixed>;
|
|
146
|
+
type UnknownRouteRecord = RouteRecord<mixed, mixed, mixed, mixed, mixed>;
|
|
99
147
|
|
|
100
148
|
/** A URL matched against a table. */
|
|
101
149
|
export type RouteMatch<TRoute: { +path: string, ... } = UnknownRouteRecord> = {|
|