@rpcbase/router 0.28.0 → 0.29.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.
@@ -0,0 +1,4 @@
1
+ import { Request } from "express";
2
+ import { StaticHandlerContext } from "./index";
3
+ export declare function applyRouteLoaders(req: Request, dataRoutes: any[]): Promise<StaticHandlerContext>;
4
+ //# sourceMappingURL=applyRouteLoaders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAE/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,SAAS,CAAA;AAsDhB,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,GAAG,EAAE,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAkG/B"}
@@ -0,0 +1,126 @@
1
+ import { matchRoutes, createPath, parsePath, } from "./index";
2
+ function createKey() {
3
+ return Math.random().toString(36).substring(2, 10);
4
+ }
5
+ function createLocation(current, to, state = null, key) {
6
+ const location = {
7
+ pathname: typeof current === "string" ? current : current.pathname,
8
+ search: "",
9
+ hash: "",
10
+ ...(typeof to === "string" ? parsePath(to) : to),
11
+ state,
12
+ // TODO: This could be cleaned up. push/replace should probably just take
13
+ // full Locations now and avoid the need to run through this flow at all
14
+ // But that's a pretty big refactor to the current test suite so going to
15
+ // keep as is for the time being and just let any incoming keys take precedence
16
+ key: (to && to.key) || key || createKey(),
17
+ };
18
+ return location;
19
+ }
20
+ function getShortCircuitMatches(routes) {
21
+ // Prefer a root layout route if present, otherwise shim in a route object
22
+ const route = routes.length === 1
23
+ ? routes[0]
24
+ : routes.find((r) => r.index || !r.path || r.path === "/") || {
25
+ id: "__shim-error-route__",
26
+ };
27
+ return {
28
+ matches: [
29
+ {
30
+ params: {},
31
+ pathname: "",
32
+ pathnameBase: "",
33
+ route,
34
+ },
35
+ ],
36
+ route,
37
+ };
38
+ }
39
+ export async function applyRouteLoaders(req, dataRoutes) {
40
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
41
+ const url = new URL(req.originalUrl, baseUrl);
42
+ const method = req.method;
43
+ const location = createLocation("", createPath(url), null, "default");
44
+ const baseContext = {
45
+ basename: "",
46
+ location,
47
+ loaderHeaders: {},
48
+ actionHeaders: {},
49
+ };
50
+ // Match routes to the current location
51
+ const matches = matchRoutes(dataRoutes, location) || [];
52
+ // Handle 404 (no matches)
53
+ if (!matches) {
54
+ const error = {
55
+ status: 404,
56
+ message: `No route matches URL: ${req.originalUrl}`,
57
+ };
58
+ const { matches: notFoundMatches, route } = getShortCircuitMatches(dataRoutes);
59
+ return {
60
+ ...baseContext,
61
+ matches: notFoundMatches,
62
+ loaderData: {},
63
+ actionData: null,
64
+ errors: { [route.id]: error },
65
+ statusCode: 404,
66
+ };
67
+ }
68
+ // Skip if anything but GET
69
+ if (method !== "GET") {
70
+ return {
71
+ ...baseContext,
72
+ matches,
73
+ loaderData: {},
74
+ actionData: null,
75
+ errors: null,
76
+ statusCode: 200,
77
+ };
78
+ }
79
+ // Collect loader data and errors
80
+ const loaderPromisesResults = await Promise.allSettled(matches.map(async (match) => {
81
+ const { route, params } = match;
82
+ if (!route.loader)
83
+ return null;
84
+ try {
85
+ return {
86
+ id: route.id,
87
+ data: await route.loader({
88
+ params,
89
+ ctx: { req },
90
+ }),
91
+ };
92
+ }
93
+ catch (error) {
94
+ // Include the route ID in the error for better traceability
95
+ throw { id: route.id, reason: error };
96
+ }
97
+ }));
98
+ const loaderData = {};
99
+ // TODO: add i18n error handling
100
+ let errors = null;
101
+ for (const result of loaderPromisesResults) {
102
+ if (result.status === "fulfilled") {
103
+ if (result.value) {
104
+ loaderData[result.value.id] = result.value.data;
105
+ }
106
+ }
107
+ else if (result.status === "rejected") {
108
+ const id = result.reason?.id;
109
+ if (!id) {
110
+ throw new Error(`missing route ID in error: ${result.reason}`);
111
+ }
112
+ if (!errors) {
113
+ errors = {};
114
+ }
115
+ errors[id] = result.reason;
116
+ }
117
+ }
118
+ return {
119
+ ...baseContext,
120
+ matches,
121
+ loaderData,
122
+ actionData: null,
123
+ errors,
124
+ statusCode: Object.keys(errors || {}).length > 0 ? 500 : 200,
125
+ };
126
+ }
@@ -0,0 +1,5 @@
1
+ export * from "react-router";
2
+ export * from "./applyRouteLoaders";
3
+ export * from "./loadRoute";
4
+ export * from "./useApplyMeta";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "react-router";
2
+ export * from "./applyRouteLoaders";
3
+ export * from "./loadRoute";
4
+ export * from "./useApplyMeta";
@@ -0,0 +1,13 @@
1
+ import { LoaderFunction } from "react-router";
2
+ import { Loader } from "@rpcbase/client";
3
+ type RouteModule = {
4
+ default: React.ComponentType<unknown>;
5
+ loader?: Loader;
6
+ };
7
+ type RouteWithLoader = {
8
+ Component: React.LazyExoticComponent<React.ComponentType<unknown>>;
9
+ loader?: LoaderFunction;
10
+ };
11
+ export declare const loadRoute: (importPromise: Promise<RouteModule>) => RouteWithLoader;
12
+ export {};
13
+ //# sourceMappingURL=loadRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadRoute.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/loadRoute.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAA;AAGpD,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,KAAK,eAAe,GAAG;IACrB,SAAS,EAAE,KAAK,CAAC,mBAAmB,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IAClE,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,eAAe,OAAO,CAAC,WAAW,CAAC,KAAG,eAa/D,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { lazy } from "react";
2
+ export const loadRoute = (importPromise) => {
3
+ const Component = lazy(async () => {
4
+ const module = await importPromise;
5
+ return { default: module.default };
6
+ });
7
+ const loader = async (args) => {
8
+ const module = await importPromise;
9
+ if (!module.loader)
10
+ return null;
11
+ return module.loader(args);
12
+ };
13
+ return { Component, loader: loader };
14
+ };
@@ -0,0 +1,2 @@
1
+ export declare const useApplyMeta: () => void;
2
+ //# sourceMappingURL=useApplyMeta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useApplyMeta.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/useApplyMeta.tsx"],"names":[],"mappings":"AAkBA,eAAO,MAAM,YAAY,YAoExB,CAAA"}
@@ -0,0 +1,56 @@
1
+ import { useEffect } from "react";
2
+ import { useLocation } from "react-router";
3
+ export const useApplyMeta = () => {
4
+ const location = useLocation();
5
+ useEffect(() => {
6
+ const loadMeta = async () => {
7
+ let defaultTitle = "";
8
+ let defaultMeta = [];
9
+ let pagesMeta = {};
10
+ try {
11
+ const importPath = "@/static/meta";
12
+ const module = (await import(importPath));
13
+ defaultTitle = module.defaultTitle || defaultTitle;
14
+ defaultMeta = module.defaultMeta || defaultMeta;
15
+ pagesMeta = module.pagesMeta || pagesMeta;
16
+ }
17
+ catch (error) {
18
+ if (import.meta.env.MODE !== "production") {
19
+ console.warn("Failed to load meta data from '@/static/meta'.", error);
20
+ }
21
+ return;
22
+ }
23
+ let pageMeta = pagesMeta[location.pathname];
24
+ if (!pageMeta) {
25
+ pageMeta = { title: defaultTitle, meta: defaultMeta };
26
+ }
27
+ document.title = pageMeta.title;
28
+ // Remove previous dynamically inserted tags
29
+ document.querySelectorAll("[data-react-meta]").forEach((tag) => tag.remove());
30
+ // Inject new tags
31
+ pageMeta.meta.forEach((meta) => {
32
+ const metaElement = document.createElement("meta");
33
+ metaElement.setAttribute("data-react-meta", "true");
34
+ // Set all attributes from the meta object
35
+ Object.entries(meta).forEach(([key, value]) => {
36
+ if (value) {
37
+ metaElement.setAttribute(key, value.toString());
38
+ }
39
+ });
40
+ document.head.appendChild(metaElement);
41
+ });
42
+ // Update canonical link
43
+ const canonicalUrl = `${window.location.origin}${location.pathname}`;
44
+ const existingCanonical = document.querySelector("link[rel=\"canonical\"]");
45
+ if (existingCanonical) {
46
+ existingCanonical.remove();
47
+ }
48
+ const canonicalLink = document.createElement("link");
49
+ canonicalLink.setAttribute("rel", "canonical");
50
+ canonicalLink.setAttribute("href", canonicalUrl);
51
+ canonicalLink.setAttribute("data-react-meta", "true");
52
+ document.head.appendChild(canonicalLink);
53
+ };
54
+ loadMeta();
55
+ }, [location.pathname]);
56
+ };
package/package.json CHANGED
@@ -1,16 +1,34 @@
1
1
  {
2
2
  "name": "@rpcbase/router",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
- "main": "./src/index.ts",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
6
10
  "scripts": {
7
- "build": "tsc --watch",
11
+ "build": "wireit",
8
12
  "release": "wireit"
9
13
  },
10
14
  "wireit": {
15
+ "build": {
16
+ "command": "node ../../scripts/build-package.js router",
17
+ "files": [
18
+ "src/**/*",
19
+ "../../tsconfig.json",
20
+ "../../tsconfig.base.json",
21
+ "../../scripts/build-package.js"
22
+ ],
23
+ "output": [
24
+ "dist/"
25
+ ]
26
+ },
11
27
  "release": {
12
28
  "command": "../../scripts/publish.js",
13
- "dependencies": [],
29
+ "dependencies": [
30
+ "build"
31
+ ],
14
32
  "files": [
15
33
  "package.json",
16
34
  "src/**/*"
@@ -1,166 +0,0 @@
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 DELETED
@@ -1,4 +0,0 @@
1
- export * from "react-router"
2
- export * from "./applyRouteLoaders"
3
- export * from "./loadRoute"
4
- export * from "./useApplyMeta"
package/src/loadRoute.tsx DELETED
@@ -1,29 +0,0 @@
1
- import { lazy } from "react"
2
- import { LoaderFunction } from "react-router"
3
- import { Loader, LoaderArgs } from "@rpcbase/client"
4
-
5
-
6
- type RouteModule = {
7
- default: React.ComponentType<unknown>
8
- loader?: Loader
9
- }
10
-
11
- type RouteWithLoader = {
12
- Component: React.LazyExoticComponent<React.ComponentType<unknown>>
13
- loader?: LoaderFunction
14
- }
15
-
16
- export const loadRoute = (importPromise: Promise<RouteModule>): RouteWithLoader => {
17
- const Component = lazy(async () => {
18
- const module = await importPromise
19
- return { default: module.default }
20
- })
21
-
22
- const loader = async (args: LoaderArgs) => {
23
- const module = await importPromise
24
- if (!module.loader) return null
25
- return module.loader(args)
26
- }
27
-
28
- return { Component, loader: loader as unknown as LoaderFunction }
29
- }
@@ -1,73 +0,0 @@
1
- import { useEffect } from "react"
2
- import { useLocation } from "react-router"
3
-
4
-
5
- export const useApplyMeta = () => {
6
- const location = useLocation()
7
-
8
- useEffect(() => {
9
- const loadMeta = async () => {
10
- let defaultTitle = ""
11
- let defaultMeta = []
12
- let pagesMeta = {}
13
-
14
- try {
15
- const importPath = "@/static/meta"
16
- const module = await import(importPath)
17
- defaultTitle = module.defaultTitle || defaultTitle
18
- defaultMeta = module.defaultMeta || defaultMeta
19
- pagesMeta = module.pagesMeta || pagesMeta
20
- } catch (error) {
21
- if (import.meta.env.MODE !== "production") {
22
- console.warn(
23
- "Failed to load meta data from '@/static/meta'.",
24
- error
25
- )
26
- }
27
- return
28
- }
29
-
30
- let pageMeta = pagesMeta[location.pathname]
31
-
32
- if (!pageMeta) {
33
- pageMeta = { title: defaultTitle, meta: defaultMeta }
34
- }
35
-
36
- document.title = pageMeta.title
37
-
38
- // Remove previous dynamically inserted tags
39
- document.querySelectorAll("[data-react-meta]").forEach((tag) => tag.remove())
40
-
41
- // Inject new tags
42
- pageMeta.meta.forEach((meta) => {
43
- const metaElement = document.createElement("meta")
44
- metaElement.setAttribute("data-react-meta", "true")
45
-
46
- // Set all attributes from the meta object
47
- Object.entries(meta).forEach(([key, value]) => {
48
- if (value) {
49
- metaElement.setAttribute(key, value.toString())
50
- }
51
- })
52
-
53
- document.head.appendChild(metaElement)
54
- })
55
-
56
- // Update canonical link
57
- const canonicalUrl = `${window.location.origin}${location.pathname}`
58
-
59
- const existingCanonical = document.querySelector("link[rel=\"canonical\"]")
60
- if (existingCanonical) {
61
- existingCanonical.remove()
62
- }
63
-
64
- const canonicalLink = document.createElement("link")
65
- canonicalLink.setAttribute("rel", "canonical")
66
- canonicalLink.setAttribute("href", canonicalUrl)
67
- canonicalLink.setAttribute("data-react-meta", "true")
68
- document.head.appendChild(canonicalLink)
69
- }
70
-
71
- loadMeta()
72
- }, [location.pathname])
73
- }