dinou 5.0.1 → 5.0.3
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/dinou/core/babel-esm-loader.js +48 -1
- 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 +117 -2
- package/dinou/core/generating-isg.js +15 -20
- package/dinou/core/get-error-jsx.js +1 -1
- package/dinou/core/get-jsx.js +1 -1
- package/dinou/core/path-utils.js +10 -0
- package/dinou/core/revalidating.js +15 -10
- package/dinou/core/server.js +232 -41
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -0
- package/dinou/package.json +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [5.0.3] - 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Native React 19 Server Actions & `callServer` Support**: Enabled full integration for submitting forms directly from Server Components using the native `<form action={ServerAction}>` pattern.
|
|
12
|
+
- **Client-side Integration**: Implemented the global `callServer` option hook within `createFromFetch` in both the ESM client (`client.jsx`) and Webpack client (`client-webpack.jsx`). React now intercepts form submits and marshals arguments through Dinou's `createServerFunctionProxy` seamlessly.
|
|
13
|
+
- **Server-side Module Loader Integration**: Updated `babel-esm-loader.js` to automatically scan `"use server"` files and register exported functions using React's `registerServerReference` runtime call.
|
|
14
|
+
- **Route-Level Soft Navigation Error Recovery**: Integrated client-side React `ErrorBoundary` support wrapping the router context. If rendering crashes during a soft navigation or initial mount (on either Server or Client components), the router captures the failure and dynamically fetches `/____rsc_payload_error____` to render the custom slot-based error page fallback, avoiding white screens and React root unmounts.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [5.0.2] - 2026-07-08
|
|
18
|
+
|
|
19
|
+
### 🛡️ Security & Route Control
|
|
20
|
+
* **Route Validation in RSC Payload Endpoints**: Unified security checks across endpoints. Direct requests to fetch RSC payloads (`/____rsc_payload____/*`) now enforce `validateParams()` and `allowISG()` checks matching the main HTML document loader behavior. This prevents direct payload scraping or scanning of blocked/invalid dynamic route parameters.
|
|
21
|
+
|
|
22
|
+
### 🐛 Bug Fixes
|
|
23
|
+
* **Sequential Compilation in ISR & ISG**: Resolved a critical race condition that caused disk cache desynchronization and persistent hydration mismatch errors.
|
|
24
|
+
* **Issue**: `Promise.all` compiled the RSC payload and HTML page in parallel. The HTML generator read the `rsc.rsc` file from disk before the new payload finished writing, saving a stale timestamp/state in the static HTML file.
|
|
25
|
+
* **Solution**: Refactored `revalidating.js` and `generating-isg.js` to execute sequentially. The RSC payload is compiled and committed first, ensuring the HTML compiler reads the freshly generated RSC file.
|
|
26
|
+
* **ESM Loader Compatibility for Slot Error Boundaries (`importModule` vs `require`)**: Fixed a server-side crash when using React client hooks inside slot error components (`error.tsx`).
|
|
27
|
+
* **Issue**: The framework used CommonJS `require()` to import the slot error components, bypassing the registered ESM custom loader (`register-loader.mjs`). This caused `"use client"` components to be evaluated as Server Components, failing with `useState is not a function`.
|
|
28
|
+
* **Solution**: Updated `get-jsx.js` and `get-error-jsx.js` to load slot error modules using `await importModule()`, ensuring they are correctly resolved as Client Component References.
|
|
29
|
+
|
|
8
30
|
## [5.0.1] - 2026-07-07
|
|
9
31
|
|
|
10
32
|
### Fixed
|
|
@@ -6,6 +6,8 @@ const createScopedName = require("./createScopedName");
|
|
|
6
6
|
const { extensionsWithDot } = require("./asset-extensions.js");
|
|
7
7
|
const { getAbsPathWithExt } = require("./get-abs-path-with-ext.js");
|
|
8
8
|
|
|
9
|
+
const { normalizePathCase } = require("./path-utils.js");
|
|
10
|
+
|
|
9
11
|
const Module = require("module");
|
|
10
12
|
const originalResolveFilename = Module._resolveFilename;
|
|
11
13
|
const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
|
|
@@ -90,7 +92,10 @@ exports.load = async function load(url, context, defaultLoad) {
|
|
|
90
92
|
} catch (e) {
|
|
91
93
|
throw e;
|
|
92
94
|
}
|
|
93
|
-
const rel = path.relative(
|
|
95
|
+
const rel = path.relative(
|
|
96
|
+
normalizePathCase(process.cwd()),
|
|
97
|
+
normalizePathCase(filename)
|
|
98
|
+
);
|
|
94
99
|
const source = fs.readFileSync(filename, "utf-8");
|
|
95
100
|
const urlToReturn = pathToFileURL(filename).href;
|
|
96
101
|
|
|
@@ -144,6 +149,48 @@ exports.load = async function load(url, context, defaultLoad) {
|
|
|
144
149
|
};
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
const { useServerRegex } = require("../constants.js");
|
|
153
|
+
const hasUseServer = useServerRegex.test(source);
|
|
154
|
+
|
|
155
|
+
if (isReactServer && hasUseServer) {
|
|
156
|
+
const parseExports = require("./parse-exports.js");
|
|
157
|
+
const exports = parseExports(source);
|
|
158
|
+
|
|
159
|
+
const { code } = await transformAsync(source, {
|
|
160
|
+
filename,
|
|
161
|
+
presets: [
|
|
162
|
+
["@babel/preset-react", { runtime: "automatic" }],
|
|
163
|
+
"@babel/preset-typescript",
|
|
164
|
+
],
|
|
165
|
+
sourceMaps: "inline",
|
|
166
|
+
ast: false,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let newSrc = code + "\n\n";
|
|
170
|
+
|
|
171
|
+
if (!isWebpack) {
|
|
172
|
+
const packageJsonPath = require.resolve("@roggc/react-server-dom-esm/package.json");
|
|
173
|
+
const serverNodePath = path.join(path.dirname(packageJsonPath), "server.node.js");
|
|
174
|
+
const serverNodeUrl = pathToFileURL(serverNodePath).href;
|
|
175
|
+
newSrc += `import pkgServer from ${JSON.stringify(serverNodeUrl)};\n`;
|
|
176
|
+
newSrc += 'const {registerServerReference} = pkgServer;\n';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const relativeFileUrl = "file:///" + rel.replace(/\\/g, "/");
|
|
180
|
+
for (const name of exports) {
|
|
181
|
+
if (name !== 'default') {
|
|
182
|
+
newSrc += `registerServerReference(${name}, ${JSON.stringify(relativeFileUrl)}, ${JSON.stringify(name)});\n`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
format: "module",
|
|
188
|
+
source: newSrc,
|
|
189
|
+
shortCircuit: true,
|
|
190
|
+
url: urlToReturn,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
147
194
|
if (ext === ".js") {
|
|
148
195
|
return {
|
|
149
196
|
format: "module",
|
|
@@ -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
|
}
|