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 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(process.cwd(), filename);
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
- const getRSCPayload = (rscKey) => {
43
+ let isInitialErrorLoad = true;
44
+
45
+ const getRSCPayload = (rscKey, isPrefetch = false) => {
42
46
  const url = rscKey.split("::")[0];
43
- // Important: url must already be normalized here
47
+ // 1. Check Idempotence (Avoids the infinite loop of React)
44
48
  if (cache.has(url)) return cache.get(url);
45
49
 
46
- const content = createFromFetch(
47
- fetch("/____rsc_payload_error____" + url, {
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: window.__DINOU_ERROR_MESSAGE__ || "Unknown Error",
55
- stack: window.__DINOU_ERROR_STACK__,
56
- name: window.__DINOU_ERROR_NAME__,
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(url, content);
62
- return content;
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
- {use(content)}
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
- const getRSCPayload = (rscKey) => {
43
+ let isInitialErrorLoad = true;
44
+
45
+ const getRSCPayload = (rscKey, isPrefetch = false) => {
42
46
  const url = rscKey.split("::")[0];
43
- // Important: url must already be normalized here
47
+ // 1. Check Idempotence (Avoids the infinite loop of React)
44
48
  if (cache.has(url)) return cache.get(url);
45
49
 
46
- const content = createFromFetch(
47
- fetch("/____rsc_payload_error____" + url, {
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: window.__DINOU_ERROR_MESSAGE__ || "Unknown Error",
55
- stack: window.__DINOU_ERROR_STACK__,
56
- name: window.__DINOU_ERROR_NAME__,
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(url, content);
62
- return content;
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
- {use(content)}
418
+ <ErrorBoundary
419
+ key={navError ? "error" : "normal"}
420
+ onError={setNavError}
421
+ >
422
+ {use(content)}
423
+ </ErrorBoundary>
253
424
  </RouterContext.Provider>
254
425
  );
255
426
  }