bertui 1.0.2 → 1.1.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/README.md +551 -132
- package/index.js +1 -1
- package/package.json +2 -2
- package/src/build/server-island-validator.js +156 -0
- package/src/build.js +468 -366
- package/src/router/Router.js +38 -5
- package/src/router/SSRRouter.js +156 -0
package/src/router/Router.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
// src/router/Router.js - SSR COMPATIBLE VERSION
|
|
1
2
|
import { useState, useEffect, createContext, useContext } from 'react';
|
|
2
3
|
|
|
3
4
|
const RouterContext = createContext(null);
|
|
4
5
|
|
|
6
|
+
// ✅ FIX: SSR-safe useRouter
|
|
5
7
|
export function useRouter() {
|
|
6
8
|
const context = useContext(RouterContext);
|
|
9
|
+
|
|
10
|
+
// During SSR (when window doesn't exist), return a mock router
|
|
11
|
+
if (typeof window === 'undefined') {
|
|
12
|
+
return {
|
|
13
|
+
pathname: '/',
|
|
14
|
+
params: {},
|
|
15
|
+
navigate: () => {},
|
|
16
|
+
currentRoute: null,
|
|
17
|
+
isSSR: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
if (!context) {
|
|
8
22
|
throw new Error('useRouter must be used within a Router component');
|
|
9
23
|
}
|
|
24
|
+
|
|
10
25
|
return context;
|
|
11
26
|
}
|
|
12
27
|
|
|
@@ -62,15 +77,18 @@ export function Router({ routes }) {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
function navigate(path) {
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
if (typeof window !== 'undefined') {
|
|
81
|
+
window.history.pushState({}, '', path);
|
|
82
|
+
matchAndSetRoute(path);
|
|
83
|
+
}
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
const routerValue = {
|
|
70
87
|
currentRoute,
|
|
71
88
|
params,
|
|
72
89
|
navigate,
|
|
73
|
-
pathname: window.location.pathname
|
|
90
|
+
pathname: typeof window !== 'undefined' ? window.location.pathname : '/',
|
|
91
|
+
isSSR: typeof window === 'undefined'
|
|
74
92
|
};
|
|
75
93
|
|
|
76
94
|
const Component = currentRoute?.component;
|
|
@@ -82,12 +100,27 @@ export function Router({ routes }) {
|
|
|
82
100
|
);
|
|
83
101
|
}
|
|
84
102
|
|
|
103
|
+
// ✅ FIX: SSR-safe Link component
|
|
85
104
|
export function Link({ to, children, ...props }) {
|
|
86
|
-
|
|
105
|
+
// Try to get router, but don't fail if it doesn't exist
|
|
106
|
+
let router;
|
|
107
|
+
try {
|
|
108
|
+
router = useRouter();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// During SSR, router might not be available
|
|
111
|
+
router = null;
|
|
112
|
+
}
|
|
87
113
|
|
|
88
114
|
function handleClick(e) {
|
|
115
|
+
// During SSR, just use normal link behavior
|
|
116
|
+
if (typeof window === 'undefined') return;
|
|
117
|
+
|
|
118
|
+
// If no router or navigate function, use normal link
|
|
119
|
+
if (!router || !router.navigate) return;
|
|
120
|
+
|
|
121
|
+
// Only prevent default if we have client-side routing
|
|
89
122
|
e.preventDefault();
|
|
90
|
-
navigate(to);
|
|
123
|
+
router.navigate(to);
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
return (
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/router/SSRRouter.jsx
|
|
2
|
+
// SSR-Compatible Router for BertUI - Works during build AND runtime
|
|
3
|
+
import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
const RouterContext = createContext(null);
|
|
6
|
+
|
|
7
|
+
// ✅ SSR-safe useRouter hook
|
|
8
|
+
export function useRouter() {
|
|
9
|
+
const context = useContext(RouterContext);
|
|
10
|
+
|
|
11
|
+
// During SSR, provide a mock router
|
|
12
|
+
if (!context) {
|
|
13
|
+
return {
|
|
14
|
+
pathname: '/',
|
|
15
|
+
params: {},
|
|
16
|
+
navigate: () => {},
|
|
17
|
+
isSSR: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ✅ SSR-safe Router component
|
|
25
|
+
export function Router({ routes, initialPath = '/' }) {
|
|
26
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
27
|
+
const [params, setParams] = useState({});
|
|
28
|
+
const [isClient, setIsClient] = useState(false);
|
|
29
|
+
|
|
30
|
+
// Detect if we're in the browser
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setIsClient(true);
|
|
33
|
+
matchAndSetRoute(window.location.pathname);
|
|
34
|
+
|
|
35
|
+
const handlePopState = () => {
|
|
36
|
+
matchAndSetRoute(window.location.pathname);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
window.addEventListener('popstate', handlePopState);
|
|
40
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
41
|
+
}, [routes]);
|
|
42
|
+
|
|
43
|
+
// Match route on server-side (SSR)
|
|
44
|
+
if (!isClient && !currentRoute) {
|
|
45
|
+
const matched = matchRoute(initialPath, routes);
|
|
46
|
+
if (matched) {
|
|
47
|
+
return React.createElement(
|
|
48
|
+
RouterContext.Provider,
|
|
49
|
+
{ value: { pathname: initialPath, params: matched.params, navigate: () => {}, isSSR: true } },
|
|
50
|
+
React.createElement(matched.route.component, { params: matched.params })
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchRoute(pathname, routesList) {
|
|
56
|
+
// Try static routes first
|
|
57
|
+
for (const route of routesList) {
|
|
58
|
+
if (route.type === 'static' && route.path === pathname) {
|
|
59
|
+
return { route, params: {} };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Try dynamic routes
|
|
64
|
+
for (const route of routesList) {
|
|
65
|
+
if (route.type === 'dynamic') {
|
|
66
|
+
const pattern = route.path.replace(/\[([^\]]+)\]/g, '([^/]+)');
|
|
67
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
68
|
+
const match = pathname.match(regex);
|
|
69
|
+
|
|
70
|
+
if (match) {
|
|
71
|
+
const paramNames = [...route.path.matchAll(/\[([^\]]+)\]/g)].map(m => m[1]);
|
|
72
|
+
const extractedParams = {};
|
|
73
|
+
paramNames.forEach((name, i) => {
|
|
74
|
+
extractedParams[name] = match[i + 1];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { route, params: extractedParams };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function matchAndSetRoute(pathname) {
|
|
86
|
+
const matched = matchRoute(pathname, routes);
|
|
87
|
+
|
|
88
|
+
if (matched) {
|
|
89
|
+
setCurrentRoute(matched.route);
|
|
90
|
+
setParams(matched.params);
|
|
91
|
+
} else {
|
|
92
|
+
setCurrentRoute(null);
|
|
93
|
+
setParams({});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function navigate(path) {
|
|
98
|
+
if (typeof window !== 'undefined') {
|
|
99
|
+
window.history.pushState({}, '', path);
|
|
100
|
+
matchAndSetRoute(path);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const routerValue = {
|
|
105
|
+
currentRoute,
|
|
106
|
+
params,
|
|
107
|
+
navigate,
|
|
108
|
+
pathname: typeof window !== 'undefined' ? window.location.pathname : initialPath,
|
|
109
|
+
isSSR: !isClient
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const Component = currentRoute?.component;
|
|
113
|
+
|
|
114
|
+
return React.createElement(
|
|
115
|
+
RouterContext.Provider,
|
|
116
|
+
{ value: routerValue },
|
|
117
|
+
Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ✅ SSR-safe Link component
|
|
122
|
+
export function Link({ to, children, ...props }) {
|
|
123
|
+
const { navigate, isSSR } = useRouter();
|
|
124
|
+
|
|
125
|
+
function handleClick(e) {
|
|
126
|
+
// Don't prevent default during SSR
|
|
127
|
+
if (isSSR) return;
|
|
128
|
+
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
navigate(to);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function NotFound() {
|
|
137
|
+
return React.createElement(
|
|
138
|
+
'div',
|
|
139
|
+
{
|
|
140
|
+
style: {
|
|
141
|
+
display: 'flex',
|
|
142
|
+
flexDirection: 'column',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
justifyContent: 'center',
|
|
145
|
+
minHeight: '100vh',
|
|
146
|
+
fontFamily: 'system-ui'
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
150
|
+
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
151
|
+
React.createElement('a', {
|
|
152
|
+
href: '/',
|
|
153
|
+
style: { color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }
|
|
154
|
+
}, 'Go home')
|
|
155
|
+
);
|
|
156
|
+
}
|