dinou 5.0.2 → 5.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/CHANGELOG.md +22 -0
- package/README.md +67 -4
- package/dinou/core/babel-esm-loader.js +48 -1
- package/dinou/core/build-static-pages.js +120 -4
- package/dinou/core/cache-revalidate.js +145 -0
- package/dinou/core/client-error-webpack.jsx +194 -23
- package/dinou/core/client-error.jsx +194 -23
- package/dinou/core/client-webpack.jsx +111 -2
- package/dinou/core/client.jsx +111 -2
- package/dinou/core/generate-static-page.js +2 -1
- package/dinou/core/generate-static-pages.js +1 -0
- package/dinou/core/generate-static-rsc.js +1 -2
- package/dinou/core/path-utils.js +10 -0
- package/dinou/core/server.js +101 -40
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -0
- package/dinou/package.json +6 -1
- package/dinou/server.d.ts +12 -0
- package/dinou/server.js +10 -0
- package/dinou/server.mjs +9 -0
- package/dinou/webpack/loaders/server-functions-loader.js +6 -1
- package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -1
- package/dinou/webpack/webpack.config.js +12 -1
- package/package.json +1 -1
|
@@ -5,11 +5,13 @@ import {
|
|
|
5
5
|
useTransition,
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
useMemo,
|
|
8
|
+
Component,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { createFromFetch } from "react-server-dom-webpack/client";
|
|
10
11
|
import { hydrateRoot } from "react-dom/client";
|
|
11
12
|
import { RouterContext } from "./navigation.js";
|
|
12
13
|
import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
|
|
14
|
+
import { createServerFunctionProxy } from "./server-function-proxy-webpack.js";
|
|
13
15
|
|
|
14
16
|
// ====================================================================
|
|
15
17
|
// 1. GLOBAL STATE (Outside the component)
|
|
@@ -38,30 +40,182 @@ const isHashChangeOnly = (finalPath) => {
|
|
|
38
40
|
);
|
|
39
41
|
};
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
let isInitialErrorLoad = true;
|
|
44
|
+
|
|
45
|
+
const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
42
46
|
const url = rscKey.split("::")[0];
|
|
43
|
-
//
|
|
47
|
+
// 1. Check Idempotence (Avoids the infinite loop of React)
|
|
44
48
|
if (cache.has(url)) return cache.get(url);
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let promise;
|
|
51
|
+
if (isInitialErrorLoad && url === getCurrentRoute()) {
|
|
52
|
+
isInitialErrorLoad = false;
|
|
53
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
54
|
+
promise = createFromFetch(
|
|
55
|
+
fetch(payloadUrl, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
error: {
|
|
62
|
+
message: window.__DINOU_ERROR_MESSAGE__ || "Unknown Error",
|
|
63
|
+
stack: window.__DINOU_ERROR_STACK__,
|
|
64
|
+
name: window.__DINOU_ERROR_NAME__,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
}).then((res) => {
|
|
68
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
69
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
70
|
+
cache.delete(url);
|
|
71
|
+
if (!isPrefetch) {
|
|
72
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
73
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
74
|
+
} else {
|
|
75
|
+
window.location.href = redirectUrl;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return new Promise(() => {});
|
|
79
|
+
}
|
|
80
|
+
return res;
|
|
81
|
+
}),
|
|
82
|
+
{
|
|
83
|
+
callServer: async (id, args) => {
|
|
84
|
+
const proxy = createServerFunctionProxy(id);
|
|
85
|
+
return proxy(...args);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
// Normal client.jsx load logic!
|
|
91
|
+
let payloadUrl = "/____rsc_payload____" + url;
|
|
92
|
+
const buildId = window.__DINOU_BUILD_ID__;
|
|
93
|
+
if (buildId) {
|
|
94
|
+
payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
|
|
95
|
+
window.__DINOU_BUILD_ID__ = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
promise = createFromFetch(
|
|
99
|
+
fetch(payloadUrl).then((res) => {
|
|
100
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
101
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
102
|
+
cache.delete(url);
|
|
103
|
+
if (!isPrefetch) {
|
|
104
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
105
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
106
|
+
} else {
|
|
107
|
+
window.location.href = redirectUrl;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return new Promise(() => {});
|
|
111
|
+
}
|
|
112
|
+
return res;
|
|
113
|
+
}),
|
|
114
|
+
{
|
|
115
|
+
callServer: async (id, args) => {
|
|
116
|
+
const proxy = createServerFunctionProxy(id);
|
|
117
|
+
return proxy(...args);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cache.set(url, promise);
|
|
124
|
+
return promise;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const getErrorRSCPayload = (route, error) => {
|
|
128
|
+
const url = route.split("::")[0];
|
|
129
|
+
const cacheKey = `error::${url}::${error.message || String(error)}`;
|
|
130
|
+
if (cache.has(cacheKey)) {
|
|
131
|
+
return cache.get(cacheKey);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
135
|
+
const promise = createFromFetch(
|
|
136
|
+
fetch(payloadUrl, {
|
|
48
137
|
method: "POST",
|
|
49
138
|
headers: {
|
|
50
139
|
"Content-Type": "application/json",
|
|
51
140
|
},
|
|
52
141
|
body: JSON.stringify({
|
|
53
142
|
error: {
|
|
54
|
-
message:
|
|
55
|
-
|
|
56
|
-
|
|
143
|
+
message: error.message || "Unknown Error",
|
|
144
|
+
name: error.name,
|
|
145
|
+
stack: error.stack,
|
|
57
146
|
},
|
|
58
147
|
}),
|
|
148
|
+
}).then((res) => {
|
|
149
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
150
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
151
|
+
cache.delete(cacheKey);
|
|
152
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
153
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
154
|
+
} else {
|
|
155
|
+
window.location.href = redirectUrl;
|
|
156
|
+
}
|
|
157
|
+
return new Promise(() => {});
|
|
158
|
+
}
|
|
159
|
+
return res;
|
|
59
160
|
}),
|
|
161
|
+
{
|
|
162
|
+
callServer: async (id, args) => {
|
|
163
|
+
const proxy = createServerFunctionProxy(id);
|
|
164
|
+
return proxy(...args);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
60
167
|
);
|
|
61
|
-
cache.set(
|
|
62
|
-
return
|
|
168
|
+
cache.set(cacheKey, promise);
|
|
169
|
+
return promise;
|
|
63
170
|
};
|
|
64
171
|
|
|
172
|
+
class ErrorBoundary extends Component {
|
|
173
|
+
constructor(props) {
|
|
174
|
+
super(props);
|
|
175
|
+
this.state = { hasError: false, error: null };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static getDerivedStateFromError(error) {
|
|
179
|
+
return { hasError: true, error };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
componentDidCatch(error, errorInfo) {
|
|
183
|
+
console.error("[Dinou] ErrorBoundary caught error:", error, errorInfo);
|
|
184
|
+
if (this.props.onError) {
|
|
185
|
+
this.props.onError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
render() {
|
|
190
|
+
if (this.state.hasError) {
|
|
191
|
+
if (this.props.fallback) {
|
|
192
|
+
return this.props.fallback(this.state.error);
|
|
193
|
+
}
|
|
194
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
195
|
+
return (
|
|
196
|
+
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
|
|
197
|
+
<h2>Application Error</h2>
|
|
198
|
+
<p>An unexpected error occurred on the client.</p>
|
|
199
|
+
<pre style={{ backgroundColor: "#f5f5f5", padding: "15px", borderRadius: "5px", overflowX: "auto" }}>
|
|
200
|
+
{isDev ? (
|
|
201
|
+
<>
|
|
202
|
+
<div style={{ fontWeight: "bold", marginBottom: "10px" }}>
|
|
203
|
+
{this.state.error?.name || "Error"}: {this.state.error?.message || String(this.state.error)}
|
|
204
|
+
</div>
|
|
205
|
+
<div>{this.state.error?.stack}</div>
|
|
206
|
+
</>
|
|
207
|
+
) : (
|
|
208
|
+
this.state.error?.message || String(this.state.error)
|
|
209
|
+
)}
|
|
210
|
+
</pre>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return this.props.children;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
65
219
|
// ====================================================================
|
|
66
220
|
// 3. ROUTER COMPONENT
|
|
67
221
|
// ====================================================================
|
|
@@ -71,18 +225,7 @@ function Router() {
|
|
|
71
225
|
const [isPopState, setIsPopState] = useState(false);
|
|
72
226
|
const [isPending, startTransition] = useTransition();
|
|
73
227
|
const [version, setVersion] = useState(0);
|
|
74
|
-
|
|
75
|
-
// 🔌 EFFECT 1: Expose Global Prefetch
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
window.__DINOU_PREFETCH__ = (url) => {
|
|
78
|
-
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
79
|
-
if (isHashChangeOnly(url)) return;
|
|
80
|
-
getRSCPayload(url);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Hydration
|
|
84
|
-
document.body.setAttribute("data-hydrated", "true");
|
|
85
|
-
}, []); // Only on mount
|
|
228
|
+
const [navError, setNavError] = useState(null);
|
|
86
229
|
|
|
87
230
|
// 🧭 NAVIGATE FUNCTION (Core Logic)
|
|
88
231
|
const navigate = (href, options = {}) => {
|
|
@@ -126,9 +269,30 @@ function Router() {
|
|
|
126
269
|
startTransition(() => {
|
|
127
270
|
setIsPopState(false);
|
|
128
271
|
setRoute(finalPath);
|
|
272
|
+
setNavError(null);
|
|
129
273
|
});
|
|
130
274
|
};
|
|
131
275
|
|
|
276
|
+
// 🔌 EFFECT 1: Expose Global Prefetch & Navigation
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
window.__DINOU_PREFETCH__ = (url) => {
|
|
279
|
+
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
280
|
+
if (isHashChangeOnly(url)) return;
|
|
281
|
+
getRSCPayload(url, true);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
window.__DINOU_ROUTER_NAVIGATE__ = navigate;
|
|
285
|
+
|
|
286
|
+
// Hydration
|
|
287
|
+
document.body.setAttribute("data-hydrated", "true");
|
|
288
|
+
|
|
289
|
+
return () => {
|
|
290
|
+
if (window.__DINOU_ROUTER_NAVIGATE__ === navigate) {
|
|
291
|
+
window.__DINOU_ROUTER_NAVIGATE__ = undefined;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}, [navigate]);
|
|
295
|
+
|
|
132
296
|
const back = () => window.history.back();
|
|
133
297
|
const forward = () => window.history.forward();
|
|
134
298
|
const refresh = () => {
|
|
@@ -142,6 +306,7 @@ function Router() {
|
|
|
142
306
|
startTransition(() => {
|
|
143
307
|
// 3. Increment version to force re-execution of useMemo
|
|
144
308
|
setVersion((v) => v + 1);
|
|
309
|
+
setNavError(null);
|
|
145
310
|
});
|
|
146
311
|
};
|
|
147
312
|
|
|
@@ -188,6 +353,7 @@ function Router() {
|
|
|
188
353
|
startTransition(() => {
|
|
189
354
|
setIsPopState(true);
|
|
190
355
|
setRoute(target);
|
|
356
|
+
setNavError(null);
|
|
191
357
|
});
|
|
192
358
|
};
|
|
193
359
|
|
|
@@ -233,7 +399,7 @@ function Router() {
|
|
|
233
399
|
|
|
234
400
|
// RSC Logic
|
|
235
401
|
const rscKey = route + "::" + version;
|
|
236
|
-
const content = getRSCPayload(rscKey);
|
|
402
|
+
const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
|
|
237
403
|
|
|
238
404
|
const contextValue = useMemo(
|
|
239
405
|
() => ({
|
|
@@ -249,7 +415,12 @@ function Router() {
|
|
|
249
415
|
|
|
250
416
|
return (
|
|
251
417
|
<RouterContext.Provider value={contextValue}>
|
|
252
|
-
|
|
418
|
+
<ErrorBoundary
|
|
419
|
+
key={navError ? "error" : "normal"}
|
|
420
|
+
onError={setNavError}
|
|
421
|
+
>
|
|
422
|
+
{use(content)}
|
|
423
|
+
</ErrorBoundary>
|
|
253
424
|
</RouterContext.Provider>
|
|
254
425
|
);
|
|
255
426
|
}
|
|
@@ -5,11 +5,13 @@ import {
|
|
|
5
5
|
useTransition,
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
useMemo,
|
|
8
|
+
Component,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { createFromFetch } from "@roggc/react-server-dom-esm/client";
|
|
10
11
|
import { hydrateRoot } from "react-dom/client";
|
|
11
12
|
import { RouterContext } from "./navigation.js";
|
|
12
13
|
import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
|
|
14
|
+
import { createServerFunctionProxy } from "./server-function-proxy.js";
|
|
13
15
|
|
|
14
16
|
// ====================================================================
|
|
15
17
|
// 1. GLOBAL STATE (Outside the component)
|
|
@@ -38,30 +40,182 @@ const isHashChangeOnly = (finalPath) => {
|
|
|
38
40
|
);
|
|
39
41
|
};
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
let isInitialErrorLoad = true;
|
|
44
|
+
|
|
45
|
+
const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
42
46
|
const url = rscKey.split("::")[0];
|
|
43
|
-
//
|
|
47
|
+
// 1. Check Idempotence (Avoids the infinite loop of React)
|
|
44
48
|
if (cache.has(url)) return cache.get(url);
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let promise;
|
|
51
|
+
if (isInitialErrorLoad && url === getCurrentRoute()) {
|
|
52
|
+
isInitialErrorLoad = false;
|
|
53
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
54
|
+
promise = createFromFetch(
|
|
55
|
+
fetch(payloadUrl, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
error: {
|
|
62
|
+
message: window.__DINOU_ERROR_MESSAGE__ || "Unknown Error",
|
|
63
|
+
stack: window.__DINOU_ERROR_STACK__,
|
|
64
|
+
name: window.__DINOU_ERROR_NAME__,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
}).then((res) => {
|
|
68
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
69
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
70
|
+
cache.delete(url);
|
|
71
|
+
if (!isPrefetch) {
|
|
72
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
73
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
74
|
+
} else {
|
|
75
|
+
window.location.href = redirectUrl;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return new Promise(() => {});
|
|
79
|
+
}
|
|
80
|
+
return res;
|
|
81
|
+
}),
|
|
82
|
+
{
|
|
83
|
+
callServer: async (id, args) => {
|
|
84
|
+
const proxy = createServerFunctionProxy(id);
|
|
85
|
+
return proxy(...args);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
// Normal client.jsx load logic!
|
|
91
|
+
let payloadUrl = "/____rsc_payload____" + url;
|
|
92
|
+
const buildId = window.__DINOU_BUILD_ID__;
|
|
93
|
+
if (buildId) {
|
|
94
|
+
payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
|
|
95
|
+
window.__DINOU_BUILD_ID__ = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
promise = createFromFetch(
|
|
99
|
+
fetch(payloadUrl).then((res) => {
|
|
100
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
101
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
102
|
+
cache.delete(url);
|
|
103
|
+
if (!isPrefetch) {
|
|
104
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
105
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
106
|
+
} else {
|
|
107
|
+
window.location.href = redirectUrl;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return new Promise(() => {});
|
|
111
|
+
}
|
|
112
|
+
return res;
|
|
113
|
+
}),
|
|
114
|
+
{
|
|
115
|
+
callServer: async (id, args) => {
|
|
116
|
+
const proxy = createServerFunctionProxy(id);
|
|
117
|
+
return proxy(...args);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cache.set(url, promise);
|
|
124
|
+
return promise;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const getErrorRSCPayload = (route, error) => {
|
|
128
|
+
const url = route.split("::")[0];
|
|
129
|
+
const cacheKey = `error::${url}::${error.message || String(error)}`;
|
|
130
|
+
if (cache.has(cacheKey)) {
|
|
131
|
+
return cache.get(cacheKey);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
135
|
+
const promise = createFromFetch(
|
|
136
|
+
fetch(payloadUrl, {
|
|
48
137
|
method: "POST",
|
|
49
138
|
headers: {
|
|
50
139
|
"Content-Type": "application/json",
|
|
51
140
|
},
|
|
52
141
|
body: JSON.stringify({
|
|
53
142
|
error: {
|
|
54
|
-
message:
|
|
55
|
-
|
|
56
|
-
|
|
143
|
+
message: error.message || "Unknown Error",
|
|
144
|
+
name: error.name,
|
|
145
|
+
stack: error.stack,
|
|
57
146
|
},
|
|
58
147
|
}),
|
|
148
|
+
}).then((res) => {
|
|
149
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
150
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
151
|
+
cache.delete(cacheKey);
|
|
152
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
153
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
154
|
+
} else {
|
|
155
|
+
window.location.href = redirectUrl;
|
|
156
|
+
}
|
|
157
|
+
return new Promise(() => {});
|
|
158
|
+
}
|
|
159
|
+
return res;
|
|
59
160
|
}),
|
|
161
|
+
{
|
|
162
|
+
callServer: async (id, args) => {
|
|
163
|
+
const proxy = createServerFunctionProxy(id);
|
|
164
|
+
return proxy(...args);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
60
167
|
);
|
|
61
|
-
cache.set(
|
|
62
|
-
return
|
|
168
|
+
cache.set(cacheKey, promise);
|
|
169
|
+
return promise;
|
|
63
170
|
};
|
|
64
171
|
|
|
172
|
+
class ErrorBoundary extends Component {
|
|
173
|
+
constructor(props) {
|
|
174
|
+
super(props);
|
|
175
|
+
this.state = { hasError: false, error: null };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static getDerivedStateFromError(error) {
|
|
179
|
+
return { hasError: true, error };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
componentDidCatch(error, errorInfo) {
|
|
183
|
+
console.error("[Dinou] ErrorBoundary caught error:", error, errorInfo);
|
|
184
|
+
if (this.props.onError) {
|
|
185
|
+
this.props.onError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
render() {
|
|
190
|
+
if (this.state.hasError) {
|
|
191
|
+
if (this.props.fallback) {
|
|
192
|
+
return this.props.fallback(this.state.error);
|
|
193
|
+
}
|
|
194
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
195
|
+
return (
|
|
196
|
+
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
|
|
197
|
+
<h2>Application Error</h2>
|
|
198
|
+
<p>An unexpected error occurred on the client.</p>
|
|
199
|
+
<pre style={{ backgroundColor: "#f5f5f5", padding: "15px", borderRadius: "5px", overflowX: "auto" }}>
|
|
200
|
+
{isDev ? (
|
|
201
|
+
<>
|
|
202
|
+
<div style={{ fontWeight: "bold", marginBottom: "10px" }}>
|
|
203
|
+
{this.state.error?.name || "Error"}: {this.state.error?.message || String(this.state.error)}
|
|
204
|
+
</div>
|
|
205
|
+
<div>{this.state.error?.stack}</div>
|
|
206
|
+
</>
|
|
207
|
+
) : (
|
|
208
|
+
this.state.error?.message || String(this.state.error)
|
|
209
|
+
)}
|
|
210
|
+
</pre>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return this.props.children;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
65
219
|
// ====================================================================
|
|
66
220
|
// 3. ROUTER COMPONENT
|
|
67
221
|
// ====================================================================
|
|
@@ -71,18 +225,7 @@ function Router() {
|
|
|
71
225
|
const [isPopState, setIsPopState] = useState(false);
|
|
72
226
|
const [isPending, startTransition] = useTransition();
|
|
73
227
|
const [version, setVersion] = useState(0);
|
|
74
|
-
|
|
75
|
-
// 🔌 EFFECT 1: Expose Global Prefetch
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
window.__DINOU_PREFETCH__ = (url) => {
|
|
78
|
-
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
79
|
-
if (isHashChangeOnly(url)) return;
|
|
80
|
-
getRSCPayload(url);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Hydration
|
|
84
|
-
document.body.setAttribute("data-hydrated", "true");
|
|
85
|
-
}, []); // Only on mount
|
|
228
|
+
const [navError, setNavError] = useState(null);
|
|
86
229
|
|
|
87
230
|
// 🧭 NAVIGATE FUNCTION (Core Logic)
|
|
88
231
|
const navigate = (href, options = {}) => {
|
|
@@ -126,9 +269,30 @@ function Router() {
|
|
|
126
269
|
startTransition(() => {
|
|
127
270
|
setIsPopState(false);
|
|
128
271
|
setRoute(finalPath);
|
|
272
|
+
setNavError(null);
|
|
129
273
|
});
|
|
130
274
|
};
|
|
131
275
|
|
|
276
|
+
// 🔌 EFFECT 1: Expose Global Prefetch & Navigation
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
window.__DINOU_PREFETCH__ = (url) => {
|
|
279
|
+
// 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
|
|
280
|
+
if (isHashChangeOnly(url)) return;
|
|
281
|
+
getRSCPayload(url, true);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
window.__DINOU_ROUTER_NAVIGATE__ = navigate;
|
|
285
|
+
|
|
286
|
+
// Hydration
|
|
287
|
+
document.body.setAttribute("data-hydrated", "true");
|
|
288
|
+
|
|
289
|
+
return () => {
|
|
290
|
+
if (window.__DINOU_ROUTER_NAVIGATE__ === navigate) {
|
|
291
|
+
window.__DINOU_ROUTER_NAVIGATE__ = undefined;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}, [navigate]);
|
|
295
|
+
|
|
132
296
|
const back = () => window.history.back();
|
|
133
297
|
const forward = () => window.history.forward();
|
|
134
298
|
const refresh = () => {
|
|
@@ -142,6 +306,7 @@ function Router() {
|
|
|
142
306
|
startTransition(() => {
|
|
143
307
|
// 3. Increment version to force re-execution of useMemo
|
|
144
308
|
setVersion((v) => v + 1);
|
|
309
|
+
setNavError(null);
|
|
145
310
|
});
|
|
146
311
|
};
|
|
147
312
|
|
|
@@ -188,6 +353,7 @@ function Router() {
|
|
|
188
353
|
startTransition(() => {
|
|
189
354
|
setIsPopState(true);
|
|
190
355
|
setRoute(target);
|
|
356
|
+
setNavError(null);
|
|
191
357
|
});
|
|
192
358
|
};
|
|
193
359
|
|
|
@@ -233,7 +399,7 @@ function Router() {
|
|
|
233
399
|
|
|
234
400
|
// RSC Logic
|
|
235
401
|
const rscKey = route + "::" + version;
|
|
236
|
-
const content = getRSCPayload(rscKey);
|
|
402
|
+
const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
|
|
237
403
|
|
|
238
404
|
const contextValue = useMemo(
|
|
239
405
|
() => ({
|
|
@@ -249,7 +415,12 @@ function Router() {
|
|
|
249
415
|
|
|
250
416
|
return (
|
|
251
417
|
<RouterContext.Provider value={contextValue}>
|
|
252
|
-
|
|
418
|
+
<ErrorBoundary
|
|
419
|
+
key={navError ? "error" : "normal"}
|
|
420
|
+
onError={setNavError}
|
|
421
|
+
>
|
|
422
|
+
{use(content)}
|
|
423
|
+
</ErrorBoundary>
|
|
253
424
|
</RouterContext.Provider>
|
|
254
425
|
);
|
|
255
426
|
}
|