@rpcbase/router 0.26.0 → 0.28.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/router",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -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
@@ -1,3 +1,4 @@
1
1
  export * from "react-router"
2
+ export * from "./applyRouteLoaders"
2
3
  export * from "./loadRoute"
3
4
  export * from "./useApplyMeta"