dinou 5.0.2 → 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 +9 -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 +111 -2
- package/dinou/core/path-utils.js +10 -0
- package/dinou/core/server.js +98 -37
- 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,15 @@ 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
|
+
|
|
8
17
|
## [5.0.2] - 2026-07-08
|
|
9
18
|
|
|
10
19
|
### 🛡️ Security & Route Control
|
|
@@ -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
|
}
|
|
@@ -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)
|
|
@@ -81,11 +83,109 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
|
81
83
|
}
|
|
82
84
|
return res;
|
|
83
85
|
}),
|
|
86
|
+
{
|
|
87
|
+
callServer: async (id, args) => {
|
|
88
|
+
const proxy = createServerFunctionProxy(id);
|
|
89
|
+
return proxy(...args);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
84
92
|
);
|
|
85
93
|
cache.set(url, promise); // <--- KEY TO AVOID LOOP
|
|
86
94
|
return promise;
|
|
87
95
|
};
|
|
88
96
|
|
|
97
|
+
const getErrorRSCPayload = (route, error) => {
|
|
98
|
+
const url = route.split("::")[0];
|
|
99
|
+
const cacheKey = `error::${url}::${error.message || String(error)}`;
|
|
100
|
+
if (cache.has(cacheKey)) {
|
|
101
|
+
return cache.get(cacheKey);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
105
|
+
const promise = createFromFetch(
|
|
106
|
+
fetch(payloadUrl, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
error: {
|
|
113
|
+
message: error.message || "Unknown Error",
|
|
114
|
+
name: error.name,
|
|
115
|
+
stack: error.stack,
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
}).then((res) => {
|
|
119
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
120
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
121
|
+
cache.delete(cacheKey);
|
|
122
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
123
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
124
|
+
} else {
|
|
125
|
+
window.location.href = redirectUrl;
|
|
126
|
+
}
|
|
127
|
+
return new Promise(() => {});
|
|
128
|
+
}
|
|
129
|
+
return res;
|
|
130
|
+
}),
|
|
131
|
+
{
|
|
132
|
+
callServer: async (id, args) => {
|
|
133
|
+
const proxy = createServerFunctionProxy(id);
|
|
134
|
+
return proxy(...args);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
cache.set(cacheKey, promise);
|
|
139
|
+
return promise;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
class ErrorBoundary extends Component {
|
|
143
|
+
constructor(props) {
|
|
144
|
+
super(props);
|
|
145
|
+
this.state = { hasError: false, error: null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static getDerivedStateFromError(error) {
|
|
149
|
+
return { hasError: true, error };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
componentDidCatch(error, errorInfo) {
|
|
153
|
+
console.error("[Dinou] ErrorBoundary caught error:", error, errorInfo);
|
|
154
|
+
if (this.props.onError) {
|
|
155
|
+
this.props.onError(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render() {
|
|
160
|
+
if (this.state.hasError) {
|
|
161
|
+
if (this.props.fallback) {
|
|
162
|
+
return this.props.fallback(this.state.error);
|
|
163
|
+
}
|
|
164
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
165
|
+
return (
|
|
166
|
+
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
|
|
167
|
+
<h2>Application Error</h2>
|
|
168
|
+
<p>An unexpected error occurred on the client.</p>
|
|
169
|
+
<pre style={{ backgroundColor: "#f5f5f5", padding: "15px", borderRadius: "5px", overflowX: "auto" }}>
|
|
170
|
+
{isDev ? (
|
|
171
|
+
<>
|
|
172
|
+
<div style={{ fontWeight: "bold", marginBottom: "10px" }}>
|
|
173
|
+
{this.state.error?.name || "Error"}: {this.state.error?.message || String(this.state.error)}
|
|
174
|
+
</div>
|
|
175
|
+
<div>{this.state.error?.stack}</div>
|
|
176
|
+
</>
|
|
177
|
+
) : (
|
|
178
|
+
this.state.error?.message || String(this.state.error)
|
|
179
|
+
)}
|
|
180
|
+
</pre>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return this.props.children;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
89
189
|
// ====================================================================
|
|
90
190
|
// 3. ROUTER COMPONENT
|
|
91
191
|
// ====================================================================
|
|
@@ -95,6 +195,7 @@ function Router() {
|
|
|
95
195
|
const [isPopState, setIsPopState] = useState(false);
|
|
96
196
|
const [isPending, startTransition] = useTransition();
|
|
97
197
|
const [version, setVersion] = useState(0);
|
|
198
|
+
const [navError, setNavError] = useState(null);
|
|
98
199
|
|
|
99
200
|
// 🧭 NAVIGATE FUNCTION (Core Logic)
|
|
100
201
|
const navigate = (href, options = {}) => {
|
|
@@ -138,6 +239,7 @@ function Router() {
|
|
|
138
239
|
startTransition(() => {
|
|
139
240
|
setIsPopState(false);
|
|
140
241
|
setRoute(finalPath);
|
|
242
|
+
setNavError(null);
|
|
141
243
|
});
|
|
142
244
|
};
|
|
143
245
|
|
|
@@ -174,6 +276,7 @@ function Router() {
|
|
|
174
276
|
startTransition(() => {
|
|
175
277
|
// 3. Increment version to force re-execution of useMemo
|
|
176
278
|
setVersion((v) => v + 1);
|
|
279
|
+
setNavError(null);
|
|
177
280
|
});
|
|
178
281
|
};
|
|
179
282
|
|
|
@@ -220,6 +323,7 @@ function Router() {
|
|
|
220
323
|
startTransition(() => {
|
|
221
324
|
setIsPopState(true);
|
|
222
325
|
setRoute(target);
|
|
326
|
+
setNavError(null);
|
|
223
327
|
});
|
|
224
328
|
};
|
|
225
329
|
|
|
@@ -265,7 +369,7 @@ function Router() {
|
|
|
265
369
|
|
|
266
370
|
// RSC Logic
|
|
267
371
|
const rscKey = route + "::" + version;
|
|
268
|
-
const content = getRSCPayload(rscKey);
|
|
372
|
+
const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
|
|
269
373
|
|
|
270
374
|
const contextValue = useMemo(
|
|
271
375
|
() => ({
|
|
@@ -281,7 +385,12 @@ function Router() {
|
|
|
281
385
|
|
|
282
386
|
return (
|
|
283
387
|
<RouterContext.Provider value={contextValue}>
|
|
284
|
-
|
|
388
|
+
<ErrorBoundary
|
|
389
|
+
key={navError ? "error" : "normal"}
|
|
390
|
+
onError={setNavError}
|
|
391
|
+
>
|
|
392
|
+
{use(content)}
|
|
393
|
+
</ErrorBoundary>
|
|
285
394
|
</RouterContext.Provider>
|
|
286
395
|
);
|
|
287
396
|
}
|
package/dinou/core/client.jsx
CHANGED
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
useTransition,
|
|
7
7
|
useLayoutEffect,
|
|
8
8
|
useMemo,
|
|
9
|
+
Component,
|
|
9
10
|
} from "react";
|
|
10
11
|
import { createFromFetch } from "@roggc/react-server-dom-esm/client";
|
|
11
12
|
import { hydrateRoot } from "react-dom/client";
|
|
12
13
|
import { RouterContext } from "./navigation.js";
|
|
13
14
|
import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
|
|
15
|
+
import { createServerFunctionProxy } from "./server-function-proxy.js";
|
|
14
16
|
|
|
15
17
|
// ====================================================================
|
|
16
18
|
// 1. GLOBAL STATE (Outside the component)
|
|
@@ -88,11 +90,109 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
|
|
|
88
90
|
}
|
|
89
91
|
return res;
|
|
90
92
|
}),
|
|
93
|
+
{
|
|
94
|
+
callServer: async (id, args) => {
|
|
95
|
+
const proxy = createServerFunctionProxy(id);
|
|
96
|
+
return proxy(...args);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
91
99
|
);
|
|
92
100
|
cache.set(url, promise); // <--- KEY TO AVOID LOOP
|
|
93
101
|
return promise;
|
|
94
102
|
};
|
|
95
103
|
|
|
104
|
+
const getErrorRSCPayload = (route, error) => {
|
|
105
|
+
const url = route.split("::")[0];
|
|
106
|
+
const cacheKey = `error::${url}::${error.message || String(error)}`;
|
|
107
|
+
if (cache.has(cacheKey)) {
|
|
108
|
+
return cache.get(cacheKey);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const payloadUrl = "/____rsc_payload_error____" + url;
|
|
112
|
+
const promise = createFromFetch(
|
|
113
|
+
fetch(payloadUrl, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
error: {
|
|
120
|
+
message: error.message || "Unknown Error",
|
|
121
|
+
name: error.name,
|
|
122
|
+
stack: error.stack,
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
}).then((res) => {
|
|
126
|
+
if (res.headers.has("x-rsc-redirect")) {
|
|
127
|
+
const redirectUrl = res.headers.get("x-rsc-redirect");
|
|
128
|
+
cache.delete(cacheKey);
|
|
129
|
+
if (window.__DINOU_ROUTER_NAVIGATE__) {
|
|
130
|
+
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
|
|
131
|
+
} else {
|
|
132
|
+
window.location.href = redirectUrl;
|
|
133
|
+
}
|
|
134
|
+
return new Promise(() => {});
|
|
135
|
+
}
|
|
136
|
+
return res;
|
|
137
|
+
}),
|
|
138
|
+
{
|
|
139
|
+
callServer: async (id, args) => {
|
|
140
|
+
const proxy = createServerFunctionProxy(id);
|
|
141
|
+
return proxy(...args);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
cache.set(cacheKey, promise);
|
|
146
|
+
return promise;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
class ErrorBoundary extends Component {
|
|
150
|
+
constructor(props) {
|
|
151
|
+
super(props);
|
|
152
|
+
this.state = { hasError: false, error: null };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static getDerivedStateFromError(error) {
|
|
156
|
+
return { hasError: true, error };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
componentDidCatch(error, errorInfo) {
|
|
160
|
+
console.error("[Dinou] ErrorBoundary caught error:", error, errorInfo);
|
|
161
|
+
if (this.props.onError) {
|
|
162
|
+
this.props.onError(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
render() {
|
|
167
|
+
if (this.state.hasError) {
|
|
168
|
+
if (this.props.fallback) {
|
|
169
|
+
return this.props.fallback(this.state.error);
|
|
170
|
+
}
|
|
171
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
172
|
+
return (
|
|
173
|
+
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
|
|
174
|
+
<h2>Application Error</h2>
|
|
175
|
+
<p>An unexpected error occurred on the client.</p>
|
|
176
|
+
<pre style={{ backgroundColor: "#f5f5f5", padding: "15px", borderRadius: "5px", overflowX: "auto" }}>
|
|
177
|
+
{isDev ? (
|
|
178
|
+
<>
|
|
179
|
+
<div style={{ fontWeight: "bold", marginBottom: "10px" }}>
|
|
180
|
+
{this.state.error?.name || "Error"}: {this.state.error?.message || String(this.state.error)}
|
|
181
|
+
</div>
|
|
182
|
+
<div>{this.state.error?.stack}</div>
|
|
183
|
+
</>
|
|
184
|
+
) : (
|
|
185
|
+
this.state.error?.message || String(this.state.error)
|
|
186
|
+
)}
|
|
187
|
+
</pre>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return this.props.children;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
96
196
|
// ====================================================================
|
|
97
197
|
// 3. ROUTER COMPONENT
|
|
98
198
|
// ====================================================================
|
|
@@ -102,6 +202,7 @@ function Router() {
|
|
|
102
202
|
const [isPopState, setIsPopState] = useState(false);
|
|
103
203
|
const [isPending, startTransition] = useTransition();
|
|
104
204
|
const [version, setVersion] = useState(0);
|
|
205
|
+
const [navError, setNavError] = useState(null);
|
|
105
206
|
|
|
106
207
|
// 🧭 NAVIGATE FUNCTION (Core Logic)
|
|
107
208
|
const navigate = (href, options = {}) => {
|
|
@@ -145,6 +246,7 @@ function Router() {
|
|
|
145
246
|
startTransition(() => {
|
|
146
247
|
setIsPopState(false);
|
|
147
248
|
setRoute(finalPath);
|
|
249
|
+
setNavError(null);
|
|
148
250
|
});
|
|
149
251
|
};
|
|
150
252
|
|
|
@@ -181,6 +283,7 @@ function Router() {
|
|
|
181
283
|
startTransition(() => {
|
|
182
284
|
// 3. Increment version to force re-execution of useMemo
|
|
183
285
|
setVersion((v) => v + 1);
|
|
286
|
+
setNavError(null);
|
|
184
287
|
});
|
|
185
288
|
};
|
|
186
289
|
|
|
@@ -227,6 +330,7 @@ function Router() {
|
|
|
227
330
|
startTransition(() => {
|
|
228
331
|
setIsPopState(true);
|
|
229
332
|
setRoute(target);
|
|
333
|
+
setNavError(null);
|
|
230
334
|
});
|
|
231
335
|
};
|
|
232
336
|
|
|
@@ -272,7 +376,7 @@ function Router() {
|
|
|
272
376
|
|
|
273
377
|
// RSC Logic
|
|
274
378
|
const rscKey = route + "::" + version;
|
|
275
|
-
const content = getRSCPayload(rscKey);
|
|
379
|
+
const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
|
|
276
380
|
|
|
277
381
|
const contextValue = useMemo(
|
|
278
382
|
() => ({
|
|
@@ -288,7 +392,12 @@ function Router() {
|
|
|
288
392
|
|
|
289
393
|
return (
|
|
290
394
|
<RouterContext.Provider value={contextValue}>
|
|
291
|
-
|
|
395
|
+
<ErrorBoundary
|
|
396
|
+
key={navError ? "error" : "normal"}
|
|
397
|
+
onError={setNavError}
|
|
398
|
+
>
|
|
399
|
+
{use(content)}
|
|
400
|
+
</ErrorBoundary>
|
|
292
401
|
</RouterContext.Provider>
|
|
293
402
|
);
|
|
294
403
|
}
|
package/dinou/core/server.js
CHANGED
|
@@ -5,6 +5,8 @@ const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
|
|
|
5
5
|
globalThis.__dinou_require__ = require;
|
|
6
6
|
const path = require("path");
|
|
7
7
|
|
|
8
|
+
const { normalizePathCase } = require("./path-utils.js");
|
|
9
|
+
|
|
8
10
|
let reactServerPath, reactDomServerPath, reactJsxRuntimePath, reactJsxDevRuntimePath;
|
|
9
11
|
|
|
10
12
|
if (!isWebpack) {
|
|
@@ -124,11 +126,10 @@ if (isDevelopment) {
|
|
|
124
126
|
while (attempts < maxRetries) {
|
|
125
127
|
try {
|
|
126
128
|
// console.log(`Attempting to load manifest (try ${attempts + 1})...`);
|
|
127
|
-
|
|
129
|
+
const text = readFileSync(manifestPath, "utf8");
|
|
130
|
+
if (!text.trim()) throw new Error("Empty JSON");
|
|
131
|
+
return JSON.parse(text);
|
|
128
132
|
} catch (err) {
|
|
129
|
-
if (err.code !== "ENOENT") {
|
|
130
|
-
throw err; // Rethrow if it's not a file not found error
|
|
131
|
-
}
|
|
132
133
|
attempts++;
|
|
133
134
|
if (attempts >= maxRetries) {
|
|
134
135
|
throw err; // Rethrow after max retries
|
|
@@ -628,7 +629,7 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
|
|
|
628
629
|
const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
629
630
|
currentGeneratedAt = metaObj.generatedAt || null;
|
|
630
631
|
}
|
|
631
|
-
} catch (e) {}
|
|
632
|
+
} catch (e) { }
|
|
632
633
|
|
|
633
634
|
const useOld =
|
|
634
635
|
isOld ||
|
|
@@ -797,7 +798,43 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
|
|
|
797
798
|
});
|
|
798
799
|
} catch (error) {
|
|
799
800
|
console.error("Error rendering RSC:", error);
|
|
800
|
-
|
|
801
|
+
try {
|
|
802
|
+
const serializedError = {
|
|
803
|
+
message: error.message || "Unknown Error",
|
|
804
|
+
name: error.name,
|
|
805
|
+
stack: isDevelopment ? error.stack : undefined,
|
|
806
|
+
};
|
|
807
|
+
const context = getContext(req, res);
|
|
808
|
+
await requestStorage.run(context, async () => {
|
|
809
|
+
const jsx = await getErrorJSX(
|
|
810
|
+
reqPath,
|
|
811
|
+
{ ...req.query },
|
|
812
|
+
serializedError,
|
|
813
|
+
isDevelopment,
|
|
814
|
+
);
|
|
815
|
+
const manifest = isDevelopment
|
|
816
|
+
? JSON.parse(
|
|
817
|
+
readFileSync(
|
|
818
|
+
path.resolve(
|
|
819
|
+
process.cwd(),
|
|
820
|
+
isWebpack
|
|
821
|
+
? `${outputFolder}/react-client-manifest.json`
|
|
822
|
+
: `react_client_manifest/react-client-manifest.json`,
|
|
823
|
+
),
|
|
824
|
+
"utf8",
|
|
825
|
+
),
|
|
826
|
+
)
|
|
827
|
+
: cachedClientManifest;
|
|
828
|
+
const { pipe } = isWebpack
|
|
829
|
+
? renderToPipeableStream(jsx, manifest)
|
|
830
|
+
: renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
|
|
831
|
+
res.status(500);
|
|
832
|
+
pipe(res);
|
|
833
|
+
});
|
|
834
|
+
} catch (innerError) {
|
|
835
|
+
console.error("Fatal error rendering fallback error RSC:", innerError);
|
|
836
|
+
res.status(500).send("Internal Server Error");
|
|
837
|
+
}
|
|
801
838
|
}
|
|
802
839
|
}
|
|
803
840
|
|
|
@@ -822,29 +859,33 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
|
|
|
822
859
|
const reqPath = (
|
|
823
860
|
req.path.endsWith("/") ? req.path : req.path + "/"
|
|
824
861
|
).replace("/____rsc_payload_error____", "");
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
862
|
+
|
|
863
|
+
const context = getContext(req, res);
|
|
864
|
+
await requestStorage.run(context, async () => {
|
|
865
|
+
const jsx = await getErrorJSX(
|
|
866
|
+
reqPath,
|
|
867
|
+
{ ...req.query },
|
|
868
|
+
req.body.error,
|
|
869
|
+
isDevelopment,
|
|
870
|
+
);
|
|
871
|
+
const manifest = isDevelopment
|
|
872
|
+
? JSON.parse(
|
|
873
|
+
readFileSync(
|
|
874
|
+
path.resolve(
|
|
875
|
+
process.cwd(),
|
|
876
|
+
isWebpack
|
|
877
|
+
? `${outputFolder}/react-client-manifest.json`
|
|
878
|
+
: `react_client_manifest/react-client-manifest.json`,
|
|
879
|
+
),
|
|
880
|
+
"utf8",
|
|
839
881
|
),
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
pipe(res);
|
|
882
|
+
)
|
|
883
|
+
: cachedClientManifest;
|
|
884
|
+
const { pipe } = isWebpack
|
|
885
|
+
? renderToPipeableStream(jsx, manifest)
|
|
886
|
+
: renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
|
|
887
|
+
pipe(res);
|
|
888
|
+
});
|
|
848
889
|
} catch (error) {
|
|
849
890
|
console.error("Error rendering RSC:", error);
|
|
850
891
|
res.status(500).send("Internal Server Error");
|
|
@@ -1001,7 +1042,7 @@ app.get(/^\/.*\/?$/, async (req, res) => {
|
|
|
1001
1042
|
const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
1002
1043
|
buildId = metaObj.generatedAt || "";
|
|
1003
1044
|
}
|
|
1004
|
-
} catch (e) {}
|
|
1045
|
+
} catch (e) { }
|
|
1005
1046
|
|
|
1006
1047
|
let scripts = `<script>window.__DINOU_USE_STATIC__=true;</script>`;
|
|
1007
1048
|
if (htmlPathOld) {
|
|
@@ -1208,8 +1249,27 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
1208
1249
|
return res.status(400).json({ error: "Invalid file URL format" });
|
|
1209
1250
|
}
|
|
1210
1251
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1252
|
+
let relativePath;
|
|
1253
|
+
|
|
1254
|
+
// Check if the URL is a relative reference (e.g. file:///src/...)
|
|
1255
|
+
// If so, extract it directly without using fileURLToPath (which throws on Windows without a drive letter)
|
|
1256
|
+
const isRelativeSrc = fileUrl.startsWith("file:///src/") || fileUrl.startsWith("file:///src\\");
|
|
1257
|
+
|
|
1258
|
+
if (isRelativeSrc) {
|
|
1259
|
+
relativePath = fileUrl.replace(/^file:\/\/\/?/, "").trim();
|
|
1260
|
+
} else {
|
|
1261
|
+
const resolvedPath = fileURLToPath(fileUrl);
|
|
1262
|
+
relativePath = resolvedPath;
|
|
1263
|
+
|
|
1264
|
+
const normalizedCwd = normalizePathCase(process.cwd());
|
|
1265
|
+
const normalizedResolved = normalizePathCase(resolvedPath);
|
|
1266
|
+
|
|
1267
|
+
if (normalizedResolved.startsWith(normalizedCwd)) {
|
|
1268
|
+
relativePath = path.relative(normalizedCwd, normalizedResolved);
|
|
1269
|
+
} else {
|
|
1270
|
+
relativePath = relativePath.replace(/^[\\/]+/, "");
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1213
1273
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
1214
1274
|
if (
|
|
1215
1275
|
normalizedRelative.startsWith("/") ||
|
|
@@ -1220,7 +1280,7 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
1220
1280
|
.status(400)
|
|
1221
1281
|
.json({ error: "Invalid path: no absolute, traversal, or drive letter allowed" });
|
|
1222
1282
|
}
|
|
1223
|
-
|
|
1283
|
+
|
|
1224
1284
|
// Restrict to 'src/' folder: prepend 'src/' if missing, and resolve absolutePath
|
|
1225
1285
|
if (!relativePath.startsWith("src/") && !relativePath.startsWith("src\\")) {
|
|
1226
1286
|
relativePath = path.join("src", relativePath);
|
|
@@ -1234,9 +1294,10 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
1234
1294
|
.status(403)
|
|
1235
1295
|
.json({ error: "Access denied: file outside src directory" });
|
|
1236
1296
|
}
|
|
1237
|
-
|
|
1297
|
+
|
|
1238
1298
|
// Verify that the file exists
|
|
1239
1299
|
if (!existsSync(absolutePath)) {
|
|
1300
|
+
// console.error("❌ [Dinou Server Function 404] File not found! id:", id, "relativePath:", relativePath, "absolutePath:", absolutePath);
|
|
1240
1301
|
return res.status(404).json({ error: "File not found" });
|
|
1241
1302
|
}
|
|
1242
1303
|
|
|
@@ -1257,8 +1318,8 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
1257
1318
|
.status(403)
|
|
1258
1319
|
.json({ error: "Not a valid server function file" });
|
|
1259
1320
|
}
|
|
1260
|
-
// Parse exports
|
|
1261
|
-
const exports = parseExports(fileContent);
|
|
1321
|
+
// Parse exports
|
|
1322
|
+
const exports = parseExports(fileContent);
|
|
1262
1323
|
allowedExports = new Set(exports);
|
|
1263
1324
|
}
|
|
1264
1325
|
|
|
@@ -1271,10 +1332,10 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
1271
1332
|
return res.status(400).json({ error: "Invalid export name" });
|
|
1272
1333
|
}
|
|
1273
1334
|
|
|
1274
|
-
// Proceed with import
|
|
1335
|
+
// Proceed with import
|
|
1275
1336
|
const mod = await importModule(absolutePath);
|
|
1276
1337
|
|
|
1277
|
-
// Validate exportName
|
|
1338
|
+
// Validate exportName
|
|
1278
1339
|
if (!exportName || (exportName !== "default" && !mod[exportName])) {
|
|
1279
1340
|
return res.status(400).json({ error: "Invalid export name" });
|
|
1280
1341
|
}
|
|
@@ -56,6 +56,9 @@ export default function getConfigEsbuildProd({
|
|
|
56
56
|
metafile: true,
|
|
57
57
|
logLevel: "warning",
|
|
58
58
|
minify: true,
|
|
59
|
+
define: {
|
|
60
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
61
|
+
},
|
|
59
62
|
external: [
|
|
60
63
|
"/__SERVER_FUNCTION_PROXY__",
|
|
61
64
|
"/serverFunctionProxy.js",
|
package/dinou/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const { normalizePathCase } = require("../../core/path-utils.js");
|
|
2
4
|
const parseExports = require("../../core/parse-exports.js");
|
|
3
5
|
const { useServerRegex } = require("../../constants.js");
|
|
4
6
|
|
|
@@ -16,7 +18,10 @@ module.exports = function (source) {
|
|
|
16
18
|
|
|
17
19
|
// Build IDs
|
|
18
20
|
const moduleId = this.resourcePath;
|
|
19
|
-
const relativePath = path.relative(
|
|
21
|
+
const relativePath = path.relative(
|
|
22
|
+
normalizePathCase(process.cwd()),
|
|
23
|
+
normalizePathCase(moduleId)
|
|
24
|
+
);
|
|
20
25
|
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
21
26
|
|
|
22
27
|
const fileUrl = `file:///${normalizedPath}`;
|
|
@@ -14,7 +14,7 @@ class ManifestGeneratorPlugin {
|
|
|
14
14
|
compilation.hooks.processAssets.tap(
|
|
15
15
|
{
|
|
16
16
|
name: pluginName,
|
|
17
|
-
stage: Compilation.
|
|
17
|
+
stage: Compilation.PROCESS_ASSETS_STAGE_ANALYSE,
|
|
18
18
|
},
|
|
19
19
|
(assets) => {
|
|
20
20
|
// Traverse chunks to generate manifest
|
|
@@ -45,7 +45,17 @@ const outputDirs = [
|
|
|
45
45
|
|
|
46
46
|
function cleanDir(dir) {
|
|
47
47
|
if (fs.existsSync(dir)) {
|
|
48
|
-
|
|
48
|
+
try {
|
|
49
|
+
const files = fs.readdirSync(dir);
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const fullPath = path.join(dir, file);
|
|
52
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
try {
|
|
56
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
} catch (err) {}
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -61,6 +71,7 @@ module.exports = async () => {
|
|
|
61
71
|
maxEntrypointSize: 512000,
|
|
62
72
|
maxAssetSize: 512000,
|
|
63
73
|
},
|
|
74
|
+
cache: false,
|
|
64
75
|
mode: isDevelopment ? "development" : "production",
|
|
65
76
|
entry: {
|
|
66
77
|
main: [path.resolve(__dirname, "../core/client-webpack.jsx")].filter(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dinou",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
4
4
|
"description": "Lightweight-Ejectable Full-Stack React 19 Framework with support for React Server Components (RSC), Server Functions, Server-Side Rendering (SSR), Static Generation (SSG), Incremental Static Generation (ISG), and Incremental Static Regeneration (ISR)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|