@rpcbase/router 0.26.0 → 0.27.0
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/package.json +1 -1
- package/src/applyRouteLoaders.ts +166 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import {Request} from "express"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
StaticHandlerContext,
|
|
6
|
+
matchRoutes,
|
|
7
|
+
createPath,
|
|
8
|
+
Location,
|
|
9
|
+
parsePath,
|
|
10
|
+
To,
|
|
11
|
+
} from "./index"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function createKey() {
|
|
15
|
+
return Math.random().toString(36).substring(2, 10)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createLocation(
|
|
19
|
+
current: string | Location,
|
|
20
|
+
to: To,
|
|
21
|
+
state: any = null,
|
|
22
|
+
key?: string,
|
|
23
|
+
): Readonly<Location> {
|
|
24
|
+
const location: Readonly<Location> = {
|
|
25
|
+
pathname: typeof current === "string" ? current : current.pathname,
|
|
26
|
+
search: "",
|
|
27
|
+
hash: "",
|
|
28
|
+
...(typeof to === "string" ? parsePath(to) : to),
|
|
29
|
+
state,
|
|
30
|
+
// TODO: This could be cleaned up. push/replace should probably just take
|
|
31
|
+
// full Locations now and avoid the need to run through this flow at all
|
|
32
|
+
// But that's a pretty big refactor to the current test suite so going to
|
|
33
|
+
// keep as is for the time being and just let any incoming keys take precedence
|
|
34
|
+
key: (to && (to as Location).key) || key || createKey(),
|
|
35
|
+
}
|
|
36
|
+
return location
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getShortCircuitMatches(routes: any[]): {
|
|
40
|
+
matches: any[];
|
|
41
|
+
route: any;
|
|
42
|
+
} {
|
|
43
|
+
// Prefer a root layout route if present, otherwise shim in a route object
|
|
44
|
+
const route =
|
|
45
|
+
routes.length === 1
|
|
46
|
+
? routes[0]
|
|
47
|
+
: routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
48
|
+
id: "__shim-error-route__",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
matches: [
|
|
53
|
+
{
|
|
54
|
+
params: {},
|
|
55
|
+
pathname: "",
|
|
56
|
+
pathnameBase: "",
|
|
57
|
+
route,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
route,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export async function applyRouteLoaders(
|
|
66
|
+
req: Request,
|
|
67
|
+
dataRoutes: any[],
|
|
68
|
+
): Promise<StaticHandlerContext> {
|
|
69
|
+
|
|
70
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`
|
|
71
|
+
const url = new URL(req.originalUrl, baseUrl)
|
|
72
|
+
|
|
73
|
+
const method = req.method
|
|
74
|
+
const location = createLocation("", createPath(url), null, "default")
|
|
75
|
+
|
|
76
|
+
const baseContext = {
|
|
77
|
+
basename: "",
|
|
78
|
+
location,
|
|
79
|
+
loaderHeaders: {},
|
|
80
|
+
actionHeaders: {},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Match routes to the current location
|
|
84
|
+
const matches = matchRoutes(dataRoutes, location) || []
|
|
85
|
+
|
|
86
|
+
// Handle 404 (no matches)
|
|
87
|
+
if (!matches) {
|
|
88
|
+
const error = {
|
|
89
|
+
status: 404,
|
|
90
|
+
message: `No route matches URL: ${req.originalUrl}`,
|
|
91
|
+
}
|
|
92
|
+
const { matches: notFoundMatches, route } = getShortCircuitMatches(dataRoutes)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...baseContext,
|
|
96
|
+
matches: notFoundMatches,
|
|
97
|
+
loaderData: {},
|
|
98
|
+
actionData: null,
|
|
99
|
+
errors: { [route.id]: error },
|
|
100
|
+
statusCode: 404,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip if anything but GET
|
|
105
|
+
if (method !== "GET") {
|
|
106
|
+
return {
|
|
107
|
+
...baseContext,
|
|
108
|
+
matches,
|
|
109
|
+
loaderData: {},
|
|
110
|
+
actionData: null,
|
|
111
|
+
errors: null,
|
|
112
|
+
statusCode: 200,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Collect loader data and errors
|
|
117
|
+
const loaderPromisesResults = await Promise.allSettled(
|
|
118
|
+
matches.map(async (match) => {
|
|
119
|
+
const { route, params } = match
|
|
120
|
+
if (!route.loader) return null
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
return {
|
|
124
|
+
id: route.id,
|
|
125
|
+
data: await route.loader({
|
|
126
|
+
params,
|
|
127
|
+
ctx: {req},
|
|
128
|
+
}),
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Include the route ID in the error for better traceability
|
|
132
|
+
throw { id: route.id, reason: error }
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const loaderData: Record<string, any> = {}
|
|
138
|
+
// TODO: add i18n error handling
|
|
139
|
+
let errors: Record<string, any> | null = null
|
|
140
|
+
|
|
141
|
+
for (const result of loaderPromisesResults) {
|
|
142
|
+
if (result.status === "fulfilled") {
|
|
143
|
+
if (result.value) {
|
|
144
|
+
loaderData[result.value.id] = result.value.data
|
|
145
|
+
}
|
|
146
|
+
} else if (result.status === "rejected") {
|
|
147
|
+
const id = result.reason?.id
|
|
148
|
+
if (!id) {
|
|
149
|
+
throw new Error(`missing route ID in error: ${result.reason}`)
|
|
150
|
+
}
|
|
151
|
+
if (!errors) {
|
|
152
|
+
errors = {}
|
|
153
|
+
}
|
|
154
|
+
errors[id] = result.reason
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...baseContext,
|
|
160
|
+
matches,
|
|
161
|
+
loaderData,
|
|
162
|
+
actionData: null,
|
|
163
|
+
errors,
|
|
164
|
+
statusCode: Object.keys(errors || {}).length > 0 ? 500 : 200,
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/index.ts
CHANGED