dinou 5.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }