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.
@@ -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
- {use(content)}
388
+ <ErrorBoundary
389
+ key={navError ? "error" : "normal"}
390
+ onError={setNavError}
391
+ >
392
+ {use(content)}
393
+ </ErrorBoundary>
285
394
  </RouterContext.Provider>
286
395
  );
287
396
  }
@@ -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)
@@ -64,6 +66,12 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
64
66
  payloadUrl = "/____rsc_payload____" + url;
65
67
  }
66
68
 
69
+ const buildId = window.__DINOU_BUILD_ID__;
70
+ if (buildId) {
71
+ payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
72
+ window.__DINOU_BUILD_ID__ = undefined;
73
+ }
74
+
67
75
  // 3. Fetch and Cache Storage
68
76
  const promise = createFromFetch(
69
77
  fetch(payloadUrl).then((res) => {
@@ -82,11 +90,109 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
82
90
  }
83
91
  return res;
84
92
  }),
93
+ {
94
+ callServer: async (id, args) => {
95
+ const proxy = createServerFunctionProxy(id);
96
+ return proxy(...args);
97
+ }
98
+ }
85
99
  );
86
100
  cache.set(url, promise); // <--- KEY TO AVOID LOOP
87
101
  return promise;
88
102
  };
89
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
+
90
196
  // ====================================================================
91
197
  // 3. ROUTER COMPONENT
92
198
  // ====================================================================
@@ -96,6 +202,7 @@ function Router() {
96
202
  const [isPopState, setIsPopState] = useState(false);
97
203
  const [isPending, startTransition] = useTransition();
98
204
  const [version, setVersion] = useState(0);
205
+ const [navError, setNavError] = useState(null);
99
206
 
100
207
  // 🧭 NAVIGATE FUNCTION (Core Logic)
101
208
  const navigate = (href, options = {}) => {
@@ -139,6 +246,7 @@ function Router() {
139
246
  startTransition(() => {
140
247
  setIsPopState(false);
141
248
  setRoute(finalPath);
249
+ setNavError(null);
142
250
  });
143
251
  };
144
252
 
@@ -175,6 +283,7 @@ function Router() {
175
283
  startTransition(() => {
176
284
  // 3. Increment version to force re-execution of useMemo
177
285
  setVersion((v) => v + 1);
286
+ setNavError(null);
178
287
  });
179
288
  };
180
289
 
@@ -221,6 +330,7 @@ function Router() {
221
330
  startTransition(() => {
222
331
  setIsPopState(true);
223
332
  setRoute(target);
333
+ setNavError(null);
224
334
  });
225
335
  };
226
336
 
@@ -266,7 +376,7 @@ function Router() {
266
376
 
267
377
  // RSC Logic
268
378
  const rscKey = route + "::" + version;
269
- const content = getRSCPayload(rscKey);
379
+ const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
270
380
 
271
381
  const contextValue = useMemo(
272
382
  () => ({
@@ -282,7 +392,12 @@ function Router() {
282
392
 
283
393
  return (
284
394
  <RouterContext.Provider value={contextValue}>
285
- {use(content)}
395
+ <ErrorBoundary
396
+ key={navError ? "error" : "normal"}
397
+ onError={setNavError}
398
+ >
399
+ {use(content)}
400
+ </ErrorBoundary>
286
401
  </RouterContext.Provider>
287
402
  );
288
403
  }
@@ -41,33 +41,28 @@ function generatingISG(reqPath, isDynamicFromServer) {
41
41
  return; // 👋 We exit without doing anything, the page is dynamic
42
42
  }
43
43
 
44
- // B. Generate HTML and RSC (Same as ISR)
45
- const [pageResult, rscResult] = await Promise.all([
46
- generateStaticPage(reqPath),
47
- generateStaticRSC(reqPath),
48
- ]);
49
-
50
- // C. Transactional Commit
51
- if (pageResult.success && rscResult.success) {
52
- await safeRename(pageResult.tempPath, pageResult.finalPath);
44
+ // B. Generate HTML and RSC sequentially (RSC first, then HTML)
45
+ const rscResult = await generateStaticRSC(reqPath);
46
+ if (!rscResult.success) {
47
+ console.warn(`⚠️ [ISG] RSC generation failed for ${reqPath}. Aborting.`);
48
+ await fs.unlink(rscResult.tempPath).catch(() => {});
49
+ return;
50
+ }
53
51
 
54
- // 2. Rename RSC (.tmp -> .rsc)
55
- // HERE is where the EPERM used to fail
56
- await safeRename(rscResult.tempPath, rscResult.finalPath);
57
- // // Rename .tmp -> Final
58
- // await fs.rename(pageResult.tempPath, pageResult.finalPath);
59
- // await fs.rename(rscResult.tempPath, rscResult.finalPath);
52
+ // Commit the RSC payload immediately so that generateStaticPage can read it
53
+ await safeRename(rscResult.tempPath, rscResult.finalPath);
60
54
 
61
- // Update Manifest
55
+ const pageResult = await generateStaticPage(reqPath);
56
+ if (pageResult.success) {
57
+ await safeRename(pageResult.tempPath, pageResult.finalPath);
62
58
  updateStatus(reqPath, pageResult.status);
63
59
  isDynamicFromServer.value = false;
64
60
  console.log(`✅ [ISG] Successfully promoted ${reqPath} to static.`);
65
61
  } else {
66
- // Cleanup if failed
67
- await fs.unlink(pageResult.tempPath).catch(() => { });
68
- await fs.unlink(rscResult.tempPath).catch(() => { });
69
- console.warn(`⚠️ [ISG] Failed to generate HTML/RSC for ${reqPath}`);
62
+ await fs.unlink(pageResult.tempPath).catch(() => {});
63
+ console.warn(`⚠️ [ISG] HTML generation failed for ${reqPath}. Aborting commit.`);
70
64
  }
65
+
71
66
  } catch (e) {
72
67
  console.error(`[ISG] Critical error promoting ${reqPath}:`, e);
73
68
  } finally {
@@ -140,7 +140,7 @@ async function getErrorJSX(reqPath, query, error, isDevelopment = false) {
140
140
  );
141
141
 
142
142
  if (slotErrorPath) {
143
- const slotErrorModule = require(slotErrorPath);
143
+ const slotErrorModule = await importModule(slotErrorPath);
144
144
  const SlotError = slotErrorModule.default ?? slotErrorModule;
145
145
 
146
146
  const serializedError = {
@@ -196,7 +196,7 @@ async function getJSX(
196
196
  );
197
197
 
198
198
  if (slotErrorPath) {
199
- const slotErrorModule = require(slotErrorPath);
199
+ const slotErrorModule = await importModule(slotErrorPath);
200
200
  const SlotError = slotErrorModule.default ?? slotErrorModule;
201
201
 
202
202
  const serializedError = {
@@ -0,0 +1,10 @@
1
+ function normalizePathCase(p) {
2
+ if (process.platform === "win32" && typeof p === "string" && p[1] === ":") {
3
+ return p.charAt(0).toLowerCase() + p.slice(1);
4
+ }
5
+ return p;
6
+ }
7
+
8
+ module.exports = {
9
+ normalizePathCase,
10
+ };
@@ -37,7 +37,9 @@ function revalidating(reqPath, isDynamicFromServer) {
37
37
  path.join(dist2Folder, reqPath, "rsc.rsc"),
38
38
  path.join(dist2Folder, reqPath, "rsc._old.rsc")
39
39
  );
40
- } catch (e) {}
40
+ } catch (e) {
41
+ console.error("[ISR] copyFileSync error:", e);
42
+ }
41
43
 
42
44
  regenerating.add(reqPath);
43
45
 
@@ -55,14 +57,19 @@ function revalidating(reqPath, isDynamicFromServer) {
55
57
  return;
56
58
  }
57
59
 
58
- const [pageResult, rscResult] = await Promise.all([
59
- generateStaticPage(reqPath),
60
- generateStaticRSC(reqPath),
61
- ]);
60
+ const rscResult = await generateStaticRSC(reqPath);
61
+ if (!rscResult.success) {
62
+ console.warn(`⚠️ [ISR] RSC generation failed for ${reqPath}. Aborting.`);
63
+ await fs.unlink(rscResult.tempPath).catch(() => {});
64
+ return;
65
+ }
62
66
 
63
- if (pageResult.success && rscResult.success) {
67
+ // Commit the RSC payload immediately so that generateStaticPage can read it
68
+ await safeRename(rscResult.tempPath, rscResult.finalPath);
69
+
70
+ const pageResult = await generateStaticPage(reqPath);
71
+ if (pageResult.success) {
64
72
  await safeRename(pageResult.tempPath, pageResult.finalPath);
65
- await safeRename(rscResult.tempPath, rscResult.finalPath);
66
73
  updateStatus(reqPath, pageResult.status);
67
74
  isDynamicFromServer.value = false;
68
75
  console.log(
@@ -70,11 +77,9 @@ function revalidating(reqPath, isDynamicFromServer) {
70
77
  );
71
78
  } else {
72
79
  console.warn(
73
- `⚠️ [ISR] Partial failure for ${reqPath}. Aborting commit.`
80
+ `⚠️ [ISR] HTML generation failed for ${reqPath}. Aborting commit.`
74
81
  );
75
-
76
82
  await fs.unlink(pageResult.tempPath).catch(() => {});
77
- await fs.unlink(rscResult.tempPath).catch(() => {});
78
83
  }
79
84
  } catch (e) {
80
85
  console.error(`[ISR] Critical error regenerating ${reqPath}:`, e);