blumenjs 0.2.0 → 0.2.1
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/cli/blumen.js +15 -5
- package/dist/cli/commands/build.js +13 -3
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/pages/BlumenStarter.tsx +1 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +49 -4
- package/dist/templates/app/shared/Link.tsx +60 -6
- package/dist/templates/app/shared/RouterContext.tsx +80 -20
- package/dist/templates/app/shared/router.ts +2 -1
- package/dist/templates/go-server/main.go +188 -1
- package/dist/templates/go-server/middleware.go +546 -0
- package/dist/templates/go-server/websocket.go +430 -0
- package/dist/templates/node-ssr/server.ts +246 -2
- package/dist/templates/scripts/generate-routes.ts +318 -10
- package/package.json +6 -4
package/dist/cli/blumen.js
CHANGED
|
@@ -243,7 +243,7 @@ async function dev() {
|
|
|
243
243
|
label: " go",
|
|
244
244
|
color: c.green,
|
|
245
245
|
cmd: "go",
|
|
246
|
-
args: ["run", "go-server/
|
|
246
|
+
args: ["run", "./go-server/"],
|
|
247
247
|
readyPattern: /Go server starting/
|
|
248
248
|
}
|
|
249
249
|
];
|
|
@@ -341,7 +341,13 @@ async function build(args = []) {
|
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
label: "Building SSR server",
|
|
344
|
-
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external"
|
|
344
|
+
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external --alias:@=./app"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
label: "Pre-rendering static pages (SSG)",
|
|
348
|
+
cmd: "npx tsx scripts/ssg-prerender.ts",
|
|
349
|
+
optional: true
|
|
350
|
+
// Don't fail if no SSG pages exist
|
|
345
351
|
}
|
|
346
352
|
];
|
|
347
353
|
const startTime = Date.now();
|
|
@@ -356,8 +362,12 @@ async function build(args = []) {
|
|
|
356
362
|
});
|
|
357
363
|
log.success(step.label);
|
|
358
364
|
} catch {
|
|
359
|
-
|
|
360
|
-
|
|
365
|
+
if (step.optional) {
|
|
366
|
+
log.success(step.label);
|
|
367
|
+
} else {
|
|
368
|
+
log.error(`Failed: ${step.label}`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
361
371
|
}
|
|
362
372
|
}
|
|
363
373
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -401,7 +411,7 @@ async function start() {
|
|
|
401
411
|
label: " go",
|
|
402
412
|
color: c.green,
|
|
403
413
|
cmd: "go",
|
|
404
|
-
args: ["run", "go-server/
|
|
414
|
+
args: ["run", "./go-server/"],
|
|
405
415
|
readyPattern: /Go server starting/
|
|
406
416
|
}
|
|
407
417
|
];
|
|
@@ -81,7 +81,13 @@ async function build(args = []) {
|
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
label: "Building SSR server",
|
|
84
|
-
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external"
|
|
84
|
+
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external --alias:@=./app"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "Pre-rendering static pages (SSG)",
|
|
88
|
+
cmd: "npx tsx scripts/ssg-prerender.ts",
|
|
89
|
+
optional: true
|
|
90
|
+
// Don't fail if no SSG pages exist
|
|
85
91
|
}
|
|
86
92
|
];
|
|
87
93
|
const startTime = Date.now();
|
|
@@ -96,8 +102,12 @@ async function build(args = []) {
|
|
|
96
102
|
});
|
|
97
103
|
log.success(step.label);
|
|
98
104
|
} catch {
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
if (step.optional) {
|
|
106
|
+
log.success(step.label);
|
|
107
|
+
} else {
|
|
108
|
+
log.error(`Failed: ${step.label}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -316,7 +316,7 @@ const HomePage: React.FC<HomeProps> = () => {
|
|
|
316
316
|
<div className="blumen-content">
|
|
317
317
|
<div className="blumen-badge">
|
|
318
318
|
<span className="blumen-badge-dot" />
|
|
319
|
-
Framework v0.
|
|
319
|
+
Framework v0.2.0
|
|
320
320
|
</div>
|
|
321
321
|
|
|
322
322
|
<h1 className="blumen-logo">🌸 Blumen</h1>
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize data for safe embedding in a <script type="application/json"> tag.
|
|
5
|
+
* Escapes all characters that could break out of the JSON context:
|
|
6
|
+
* - `<` → `\u003c` (prevents </script> injection)
|
|
7
|
+
* - `>` → `\u003e` (prevents HTML entity injection)
|
|
8
|
+
* - `&` → `\u0026` (prevents HTML entity injection)
|
|
9
|
+
* - U+2028 → `\u2028` (line separator - breaks JS in some contexts)
|
|
10
|
+
* - U+2029 → `\u2029` (paragraph separator - breaks JS in some contexts)
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeForHydration(data: any): string {
|
|
13
|
+
return JSON.stringify(data)
|
|
14
|
+
.replace(/</g, '\\u003c')
|
|
15
|
+
.replace(/>/g, '\\u003e')
|
|
16
|
+
.replace(/&/g, '\\u0026')
|
|
17
|
+
.replace(/\u2028/g, '\\u2028')
|
|
18
|
+
.replace(/\u2029/g, '\\u2029');
|
|
19
|
+
}
|
|
3
20
|
// HMR: In development, load the client bundle from Webpack Dev Server
|
|
4
21
|
// so the HMR runtime + React Fast Refresh are active.
|
|
5
22
|
const isDev = process.env.NODE_ENV === "development";
|
|
@@ -7,7 +24,7 @@ const BUNDLE_SRC = isDev
|
|
|
7
24
|
? "http://localhost:3100/static/js/bundle.js"
|
|
8
25
|
: "/static/js/bundle.js";
|
|
9
26
|
|
|
10
|
-
export function DefaultDocument({ children, initialProps }: any) {
|
|
27
|
+
export function DefaultDocument({ children, initialProps, metadata }: any) {
|
|
11
28
|
const css = `
|
|
12
29
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
13
30
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; color: #e2e8f0; background: #1a1025; }
|
|
@@ -51,13 +68,41 @@ export function DefaultDocument({ children, initialProps }: any) {
|
|
|
51
68
|
.page-transition-exit { opacity: 0; transition: opacity 150ms ease-out; }
|
|
52
69
|
`;
|
|
53
70
|
|
|
71
|
+
// Resolve metadata with defaults
|
|
72
|
+
const meta = metadata || {};
|
|
73
|
+
const title = meta.title || "Blumen App";
|
|
74
|
+
const description = meta.description || "";
|
|
75
|
+
const og = meta.openGraph || {};
|
|
76
|
+
const tw = meta.twitter || {};
|
|
77
|
+
|
|
54
78
|
return (
|
|
55
79
|
<html lang="en">
|
|
56
80
|
<head>
|
|
57
81
|
<meta charSet="UTF-8" />
|
|
58
82
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
59
|
-
<
|
|
60
|
-
<
|
|
83
|
+
<title>{title}</title>
|
|
84
|
+
{description && <meta name="description" content={description} />}
|
|
85
|
+
{meta.keywords?.length && <meta name="keywords" content={meta.keywords.join(", ")} />}
|
|
86
|
+
{meta.robots && <meta name="robots" content={meta.robots} />}
|
|
87
|
+
{meta.canonical && <link rel="canonical" href={meta.canonical} />}
|
|
88
|
+
{/* Open Graph */}
|
|
89
|
+
{og.title && <meta property="og:title" content={og.title} />}
|
|
90
|
+
{og.description && <meta property="og:description" content={og.description} />}
|
|
91
|
+
{og.image && <meta property="og:image" content={og.image} />}
|
|
92
|
+
{og.url && <meta property="og:url" content={og.url} />}
|
|
93
|
+
{og.type && <meta property="og:type" content={og.type} />}
|
|
94
|
+
{og.siteName && <meta property="og:site_name" content={og.siteName} />}
|
|
95
|
+
{/* Twitter Card */}
|
|
96
|
+
{tw.card && <meta name="twitter:card" content={tw.card} />}
|
|
97
|
+
{tw.title && <meta name="twitter:title" content={tw.title} />}
|
|
98
|
+
{tw.description && <meta name="twitter:description" content={tw.description} />}
|
|
99
|
+
{tw.image && <meta name="twitter:image" content={tw.image} />}
|
|
100
|
+
{tw.creator && <meta name="twitter:creator" content={tw.creator} />}
|
|
101
|
+
{tw.site && <meta name="twitter:site" content={tw.site} />}
|
|
102
|
+
{/* Custom meta tags */}
|
|
103
|
+
{meta.other && Object.entries(meta.other).map(([name, content]: [string, any]) => (
|
|
104
|
+
<meta key={name} name={name} content={content} />
|
|
105
|
+
))}
|
|
61
106
|
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
62
107
|
</head>
|
|
63
108
|
<body>
|
|
@@ -66,7 +111,7 @@ export function DefaultDocument({ children, initialProps }: any) {
|
|
|
66
111
|
id="ssr-props"
|
|
67
112
|
type="application/json"
|
|
68
113
|
dangerouslySetInnerHTML={{
|
|
69
|
-
__html:
|
|
114
|
+
__html: sanitizeForHydration(initialProps)
|
|
70
115
|
}}
|
|
71
116
|
/>
|
|
72
117
|
<script src={BUNDLE_SRC} defer></script>
|
|
@@ -5,21 +5,26 @@
|
|
|
5
5
|
* External links, new-tab links, and modified clicks (ctrl/cmd)
|
|
6
6
|
* are passed through to the browser as normal.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Prefetching:
|
|
9
|
+
* - On hover: preloads page data so navigation is instant
|
|
10
|
+
* - On viewport: uses IntersectionObserver to prefetch when visible
|
|
11
|
+
* - Set prefetch={false} to disable for specific links
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
* <Link href="/about" className="nav-link">About</Link>
|
|
13
|
+
* During SSR (no RouterProvider), renders a plain <a> tag.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import React, { useContext } from "react";
|
|
16
|
+
import React, { useContext, useRef, useEffect, useCallback } from "react";
|
|
15
17
|
|
|
16
18
|
// Import the context directly to do a safe check without throwing
|
|
17
19
|
// We need the raw context object, not the hook
|
|
18
20
|
import { RouterContextRef, useRouter } from "./RouterContext";
|
|
21
|
+
import { prefetch as prefetchData } from "./prefetchCache";
|
|
19
22
|
|
|
20
23
|
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
21
24
|
/** The destination path */
|
|
22
25
|
href: string;
|
|
26
|
+
/** Enable/disable prefetching. Defaults to true. Set to false for rarely-visited links. */
|
|
27
|
+
prefetch?: boolean;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function isExternal(href: string): boolean {
|
|
@@ -30,9 +35,51 @@ function isModifiedClick(e: React.MouseEvent): boolean {
|
|
|
30
35
|
return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
|
|
38
|
+
export function Link({ href, children, onClick, target, prefetch = true, ...rest }: LinkProps) {
|
|
34
39
|
// Safe check: if we're in SSR (no provider), just render a plain <a>
|
|
35
40
|
const ctx = useContext(RouterContextRef);
|
|
41
|
+
const anchorRef = useRef<HTMLAnchorElement>(null);
|
|
42
|
+
const hasPrefetched = useRef(false);
|
|
43
|
+
|
|
44
|
+
// Determine if this is a local link that can be prefetched
|
|
45
|
+
const isLocal = !isExternal(href) && target !== "_blank";
|
|
46
|
+
|
|
47
|
+
// ── Hover prefetch ───────────────────────────────────────────
|
|
48
|
+
const handleMouseEnter = useCallback(() => {
|
|
49
|
+
if (!isLocal || !prefetch || hasPrefetched.current) return;
|
|
50
|
+
hasPrefetched.current = true;
|
|
51
|
+
prefetchData(href);
|
|
52
|
+
}, [href, isLocal, prefetch]);
|
|
53
|
+
|
|
54
|
+
// ── Viewport prefetch (IntersectionObserver) ─────────────────
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!isLocal || !prefetch || !anchorRef.current) return;
|
|
57
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
58
|
+
|
|
59
|
+
const el = anchorRef.current;
|
|
60
|
+
|
|
61
|
+
const observer = new IntersectionObserver(
|
|
62
|
+
(entries) => {
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.isIntersecting && !hasPrefetched.current) {
|
|
65
|
+
hasPrefetched.current = true;
|
|
66
|
+
prefetchData(href);
|
|
67
|
+
observer.unobserve(el);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
// Start prefetching when the link is within 200px of the viewport
|
|
73
|
+
rootMargin: "200px",
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
observer.observe(el);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
observer.unobserve(el);
|
|
81
|
+
};
|
|
82
|
+
}, [href, isLocal, prefetch]);
|
|
36
83
|
|
|
37
84
|
if (!ctx) {
|
|
38
85
|
// SSR fallback — no router available
|
|
@@ -64,7 +111,14 @@ export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
|
|
|
64
111
|
};
|
|
65
112
|
|
|
66
113
|
return (
|
|
67
|
-
<a
|
|
114
|
+
<a
|
|
115
|
+
ref={anchorRef}
|
|
116
|
+
href={href}
|
|
117
|
+
onClick={handleClick}
|
|
118
|
+
onMouseEnter={handleMouseEnter}
|
|
119
|
+
target={target}
|
|
120
|
+
{...rest}
|
|
121
|
+
>
|
|
68
122
|
{children}
|
|
69
123
|
</a>
|
|
70
124
|
);
|
|
@@ -17,6 +17,9 @@ import React, {
|
|
|
17
17
|
} from "react";
|
|
18
18
|
import { matchRoute, type RouteDef } from "./router";
|
|
19
19
|
import { BlumenErrorBoundary } from "./ErrorBoundary";
|
|
20
|
+
import { applyMetadata } from "./BlumenHead";
|
|
21
|
+
import { consumeCached } from "./prefetchCache";
|
|
22
|
+
import DefaultLoading from "./DefaultLoading";
|
|
20
23
|
|
|
21
24
|
// ── Context types ──────────────────────────────────────────────
|
|
22
25
|
|
|
@@ -74,6 +77,8 @@ export function RouterProvider({
|
|
|
74
77
|
() => window.location.pathname,
|
|
75
78
|
);
|
|
76
79
|
const [transitioning, setTransitioning] = useState(false);
|
|
80
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
81
|
+
const [loadingComponent, setLoadingComponent] = useState<React.ComponentType<any> | null>(null);
|
|
77
82
|
const [dynamicProps, setDynamicProps] = useState<Record<string, any>>({});
|
|
78
83
|
|
|
79
84
|
// Track whether we're on the very first render (SSR hydration).
|
|
@@ -110,25 +115,48 @@ export function RouterProvider({
|
|
|
110
115
|
// Animate out → change route → animate in
|
|
111
116
|
setTransitioning(true);
|
|
112
117
|
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// Check prefetch cache first (populated by <Link> on hover)
|
|
119
|
+
const cached = consumeCached(to);
|
|
120
|
+
|
|
121
|
+
// Determine the loading component for the target route
|
|
122
|
+
const targetMatch = matchRoute(to, routes);
|
|
123
|
+
const targetLoading = targetMatch?.loading || null;
|
|
124
|
+
|
|
125
|
+
// If we don't have cached data and route has a loading component, show it
|
|
126
|
+
if (!cached && targetLoading) {
|
|
127
|
+
setLoadingComponent(() => targetLoading);
|
|
128
|
+
setIsLoading(true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fetch dynamic data from Go server (or use cached data)
|
|
132
|
+
const fetchPromise = cached
|
|
133
|
+
? Promise.resolve(cached)
|
|
134
|
+
: fetch(to, {
|
|
135
|
+
headers: { "X-Blumen-Data": "1" }
|
|
136
|
+
}).then(res => res.ok ? res.json() : {}).catch(err => {
|
|
137
|
+
console.error("Failed to fetch route data", err);
|
|
138
|
+
return {};
|
|
139
|
+
});
|
|
120
140
|
|
|
121
141
|
// Allow the exit transition to play (150ms)
|
|
122
142
|
const delayPromise = new Promise(resolve => setTimeout(resolve, 150));
|
|
123
143
|
|
|
124
|
-
Promise.all([fetchPromise, delayPromise]).then(([newData]) => {
|
|
144
|
+
Promise.all([fetchPromise, delayPromise]).then(([newData]: any[]) => {
|
|
125
145
|
setDynamicProps(newData);
|
|
146
|
+
|
|
147
|
+
// Update <head> metadata for the new page
|
|
148
|
+
if (newData?.metadata) {
|
|
149
|
+
applyMetadata(newData.metadata);
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
window.history.pushState(null, "", to);
|
|
127
153
|
setPath(to);
|
|
128
154
|
window.scrollTo(0, 0);
|
|
129
155
|
|
|
130
|
-
//
|
|
156
|
+
// Clear loading state and transition
|
|
131
157
|
requestAnimationFrame(() => {
|
|
158
|
+
setIsLoading(false);
|
|
159
|
+
setLoadingComponent(null);
|
|
132
160
|
setTransitioning(false);
|
|
133
161
|
});
|
|
134
162
|
});
|
|
@@ -142,17 +170,38 @@ export function RouterProvider({
|
|
|
142
170
|
setTransitioning(true);
|
|
143
171
|
const to = window.location.pathname;
|
|
144
172
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
173
|
+
// Check prefetch cache for back/forward navigation
|
|
174
|
+
const cached = consumeCached(to);
|
|
175
|
+
|
|
176
|
+
// Show loading component if available
|
|
177
|
+
if (!cached) {
|
|
178
|
+
const targetMatch = matchRoute(to, routes);
|
|
179
|
+
if (targetMatch?.loading) {
|
|
180
|
+
setLoadingComponent(() => targetMatch.loading!);
|
|
181
|
+
setIsLoading(true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fetchPromise = cached
|
|
186
|
+
? Promise.resolve(cached)
|
|
187
|
+
: fetch(to, {
|
|
188
|
+
headers: { "X-Blumen-Data": "1" }
|
|
189
|
+
}).then(res => res.ok ? res.json() : {}).catch(() => ({}));
|
|
148
190
|
|
|
149
191
|
const delayPromise = new Promise(resolve => setTimeout(resolve, 150));
|
|
150
192
|
|
|
151
|
-
Promise.all([fetchPromise, delayPromise]).then(([newData]) => {
|
|
193
|
+
Promise.all([fetchPromise, delayPromise]).then(([newData]: any[]) => {
|
|
152
194
|
setDynamicProps(newData);
|
|
153
195
|
setPath(to);
|
|
196
|
+
|
|
197
|
+
// Update <head> metadata for the new page
|
|
198
|
+
if (newData?.metadata) {
|
|
199
|
+
applyMetadata(newData.metadata);
|
|
200
|
+
}
|
|
154
201
|
|
|
155
202
|
requestAnimationFrame(() => {
|
|
203
|
+
setIsLoading(false);
|
|
204
|
+
setLoadingComponent(null);
|
|
156
205
|
setTransitioning(false);
|
|
157
206
|
});
|
|
158
207
|
});
|
|
@@ -165,15 +214,26 @@ export function RouterProvider({
|
|
|
165
214
|
// ── Render ──────────────────────────────────────────────────
|
|
166
215
|
const contextValue: RouterContextValue = { path, params, navigate };
|
|
167
216
|
|
|
217
|
+
// Determine what to render:
|
|
218
|
+
// 1. If loading — show the loading component
|
|
219
|
+
// 2. Otherwise — show the page component
|
|
220
|
+
const LoadingComp = loadingComponent;
|
|
221
|
+
|
|
168
222
|
return (
|
|
169
223
|
<RouterContext.Provider value={contextValue}>
|
|
170
|
-
|
|
171
|
-
className=
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
{isLoading && LoadingComp ? (
|
|
225
|
+
<div className="page-transition page-transition-active">
|
|
226
|
+
<LoadingComp />
|
|
227
|
+
</div>
|
|
228
|
+
) : (
|
|
229
|
+
<div
|
|
230
|
+
className={`page-transition ${transitioning ? "page-transition-exit" : "page-transition-active"}`}
|
|
231
|
+
>
|
|
232
|
+
<BlumenErrorBoundary>
|
|
233
|
+
<App Component={PageComponent} pageProps={pageProps} />
|
|
234
|
+
</BlumenErrorBoundary>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
177
237
|
</RouterContext.Provider>
|
|
178
238
|
);
|
|
179
239
|
}
|
|
@@ -3,6 +3,7 @@ export interface RouteDef {
|
|
|
3
3
|
pattern: RegExp;
|
|
4
4
|
keys: string[];
|
|
5
5
|
component: React.ComponentType<any>;
|
|
6
|
+
loading?: React.ComponentType<any>;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function matchRoute(path: string, routes: RouteDef[]) {
|
|
@@ -16,7 +17,7 @@ export function matchRoute(path: string, routes: RouteDef[]) {
|
|
|
16
17
|
route.keys.forEach((key: string, index: number) => {
|
|
17
18
|
params[key] = match[index + 1];
|
|
18
19
|
});
|
|
19
|
-
return { component: route.component, params };
|
|
20
|
+
return { component: route.component, params, loading: route.loading };
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
return null;
|