dinou 6.1.0 → 7.0.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +9461 -1080
  3. package/cli.js +392 -169
  4. package/dinou/adapters/bun.js +62 -0
  5. package/dinou/adapters/cloudflare.js +65 -0
  6. package/dinou/adapters/deno.js +48 -0
  7. package/dinou/adapters/netlify.js +85 -0
  8. package/dinou/adapters/node.mjs +31 -0
  9. package/dinou/bun/build.mjs +1077 -0
  10. package/dinou/cloudflare/build.mjs +1121 -0
  11. package/dinou/cloudflare/shims/child_process.js +14 -0
  12. package/dinou/cloudflare/shims/empty.js +2 -0
  13. package/dinou/cloudflare/shims/fs.js +99 -0
  14. package/dinou/cloudflare/shims/os.js +29 -0
  15. package/dinou/config.js +1 -0
  16. package/dinou/config.mjs +2 -0
  17. package/dinou/constants.js +29 -4
  18. package/dinou/core/babel-esm-loader.js +66 -37
  19. package/dinou/core/bailout-proxy.js +61 -0
  20. package/dinou/core/build-static-pages.js +309 -157
  21. package/dinou/core/cache-revalidate.js +471 -144
  22. package/dinou/core/cache-slot.js +387 -0
  23. package/dinou/core/client-redirect.jsx +12 -7
  24. package/dinou/core/client-webpack.jsx +457 -82
  25. package/dinou/core/client.jsx +452 -82
  26. package/dinou/core/config.js +11 -0
  27. package/dinou/core/dinou-paths.js +52 -0
  28. package/dinou/core/edge-ssr.js +122 -0
  29. package/dinou/core/export-static.mjs +64 -0
  30. package/dinou/core/generate-route-types.js +252 -0
  31. package/dinou/core/generate-static-page.js +55 -94
  32. package/dinou/core/generate-static-rsc.js +47 -15
  33. package/dinou/core/generate-static.js +212 -10
  34. package/dinou/core/generating-isg.js +5 -0
  35. package/dinou/core/get-asset-from-manifest.js +9 -3
  36. package/dinou/core/get-error-jsx.js +221 -41
  37. package/dinou/core/get-file-path-and-dynamic-params.js +9 -13
  38. package/dinou/core/get-jsx.js +172 -95
  39. package/dinou/core/handler.js +2296 -0
  40. package/dinou/core/http-adapter.js +115 -0
  41. package/dinou/core/import-module.js +83 -53
  42. package/dinou/core/layout-functions.js +181 -0
  43. package/dinou/core/link.jsx +18 -6
  44. package/dinou/core/load-dinou-config.js +60 -0
  45. package/dinou/core/manifest-provider.js +261 -0
  46. package/dinou/core/navigation-utils.js +4 -0
  47. package/dinou/core/navigation.js +50 -39
  48. package/dinou/core/parse-exports.js +207 -2
  49. package/dinou/core/ppr-context.js +105 -0
  50. package/dinou/core/ppr-runtime.js +263 -0
  51. package/dinou/core/redirect.jsx +20 -19
  52. package/dinou/core/render-jsx-to-client-jsx.js +133 -15
  53. package/dinou/core/request-context.js +47 -13
  54. package/dinou/core/resolve-module-ppr.js +32 -0
  55. package/dinou/core/revalidating.js +28 -9
  56. package/dinou/core/route-generator.js +107 -0
  57. package/dinou/core/rsc-renderer.js +90 -0
  58. package/dinou/core/run-ssg.js +19 -0
  59. package/dinou/core/scan-dependency-components.js +218 -0
  60. package/dinou/core/server-function-proxy-webpack.js +3 -0
  61. package/dinou/core/server-function-proxy.js +3 -0
  62. package/dinou/core/slot.js +302 -0
  63. package/dinou/core/status-manifest.js +41 -1
  64. package/dinou/core/storage-adapter.js +522 -0
  65. package/dinou/core/url-resolver.js +16 -16
  66. package/dinou/core/vfs.js +139 -68
  67. package/dinou/deno/build.mjs +1112 -0
  68. package/dinou/deno/seed-kv.js +115 -0
  69. package/dinou/esbuild/build.mjs +51 -1
  70. package/dinou/esbuild/dev.mjs +290 -224
  71. package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -2
  72. package/dinou/esbuild/helpers-esbuild/get-config-esbuild.mjs +104 -16
  73. package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +31 -19
  74. package/dinou/esbuild/helpers-esbuild/strip-namespace.mjs +9 -0
  75. package/dinou/esbuild/helpers-esbuild/swc-disk-cache.mjs +225 -0
  76. package/dinou/esbuild/helpers-esbuild/update-manifest-for-module.mjs +7 -0
  77. package/dinou/esbuild/helpers-esbuild/write.mjs +111 -2
  78. package/dinou/esbuild/plugins-esbuild/assets-plugin.mjs +236 -161
  79. package/dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs +101 -5
  80. package/dinou/esbuild/plugins-esbuild/react-client-manifest-plugin.mjs +63 -24
  81. package/dinou/esbuild/plugins-esbuild/server-functions-plugin.mjs +177 -56
  82. package/dinou/esbuild/plugins-esbuild/skip-missing-entry-points-plugin.mjs +3 -0
  83. package/dinou/esbuild/plugins-esbuild/stable-chunk-names-and-maps-plugin.mjs +182 -137
  84. package/dinou/esbuild/plugins-postcss/postcss-extract-plugin.js +38 -11
  85. package/dinou/esbuild/react-refresh/esm-hmr/client.mjs +55 -8
  86. package/dinou/esbuild/react-refresh/esm-hmr/server.js +1 -1
  87. package/dinou/esbuild/react-refresh/esm-hmr-plugin.mjs +238 -101
  88. package/dinou/esbuild/react-refresh/is-react-refresh-boundary.mjs +18 -4
  89. package/dinou/index.d.ts +315 -11
  90. package/dinou/index.js +12 -6
  91. package/dinou/index.mjs +9 -0
  92. package/dinou/node/build.mjs +195 -0
  93. package/dinou/node/bundle-dual-engine.mjs +1012 -0
  94. package/dinou/node/dev.mjs +2589 -0
  95. package/dinou/node/port-selector.mjs +164 -0
  96. package/dinou/node/terminal-status.mjs +345 -0
  97. package/dinou/package.json +17 -14
  98. package/dinou/rollup/react-refresh/esm-hmr/client.js +66 -10
  99. package/dinou/rollup/react-refresh/esm-hmr/server.js +1 -1
  100. package/dinou/rollup/react-refresh/is-react-refresh-boundary.js +24 -5
  101. package/dinou/rollup/react-refresh/react-refresh-runtime.js +13 -6
  102. package/dinou/rollup/react-refresh/react-refresh-wrap-modules.js +56 -11
  103. package/dinou/rollup/react-refresh/rollup-plugin-esm-hmr.js +176 -48
  104. package/dinou/rollup/rollup-plugins/dinou-asset-plugin.js +9 -5
  105. package/dinou/rollup/rollup-plugins/rollup-plugin-memory.js +66 -0
  106. package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +468 -123
  107. package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +44 -12
  108. package/dinou/rollup/rollup-plugins/rollup-plugin-swc.js +100 -0
  109. package/dinou/rollup/rollup.config.js +155 -43
  110. package/dinou/server.d.ts +114 -12
  111. package/dinou/server.js +35 -10
  112. package/dinou/server.mjs +30 -9
  113. package/dinou/webpack/helpers/get-webpack-entries.js +14 -19
  114. package/dinou/webpack/loaders/server-functions-loader.js +22 -1
  115. package/dinou/webpack/loaders/swc-loader.js +76 -0
  116. package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -0
  117. package/dinou/webpack/plugins/server-functions-plugin.js +75 -19
  118. package/dinou/webpack/plugins/webpack-memory-plugin.js +66 -0
  119. package/dinou/webpack/webpack.config.js +227 -29
  120. package/eject.js +173 -94
  121. package/package.json +110 -107
  122. package/dinou/core/asset-require-hook.js +0 -58
  123. package/dinou/core/client-error-webpack.jsx +0 -432
  124. package/dinou/core/client-error.jsx +0 -432
  125. package/dinou/core/concurrency-manager.js +0 -48
  126. package/dinou/core/context-proxy.js +0 -68
  127. package/dinou/core/generate-static-pages.js +0 -130
  128. package/dinou/core/generate-static-rscs.js +0 -151
  129. package/dinou/core/get-ssg-metadata.js +0 -31
  130. package/dinou/core/register-loader.mjs +0 -11
  131. package/dinou/core/register-paths.js +0 -27
  132. package/dinou/core/render-app-to-html.js +0 -430
  133. package/dinou/core/render-html.js +0 -407
  134. package/dinou/core/server.js +0 -1542
  135. package/dinou/esbuild/plugins-esbuild/write-metafile-plugin.mjs +0 -16
@@ -1,31 +1,104 @@
1
1
  // dinou/core/client.jsx
2
+ if (typeof window !== "undefined") {
3
+ if (!window.$RefreshReg$) window.$RefreshReg$ = () => { };
4
+ if (!window.$RefreshSig$) window.$RefreshSig$ = () => (type) => type;
5
+ }
2
6
  import {
3
7
  use,
8
+ useRef,
4
9
  useState,
5
10
  useEffect,
6
11
  useTransition,
7
12
  useLayoutEffect,
8
13
  useMemo,
14
+ useCallback,
9
15
  Component,
16
+ createElement,
17
+ StrictMode,
10
18
  } from "react";
11
19
  import { createFromFetch } from "@roggc/react-server-dom-esm/client";
12
20
  import { hydrateRoot } from "react-dom/client";
13
21
  import { RouterContext } from "./navigation.js";
14
22
  import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
15
23
  import { createServerFunctionProxy } from "./server-function-proxy.js";
24
+ import { DinouPageContext, DinouPageSlot, cleanErrorStack } from "./slot.js";
16
25
 
17
26
  // ====================================================================
18
27
  // 1. GLOBAL STATE (Outside the component)
19
28
  // ====================================================================
20
- const cache = new Map();
29
+ const pageCache = new Map();
30
+ const layoutCache = new Map();
21
31
  const scrollCache = new Map();
22
32
 
23
33
  const getCurrentRoute = () => window.location.pathname + window.location.search;
24
34
 
35
+ const scrollToCurrentHash = (rawHash = typeof window !== "undefined" ? window.location.hash : "") => {
36
+ if (!rawHash) return false;
37
+ try {
38
+ const id = decodeURIComponent(rawHash.replace("#", ""));
39
+ const element = document.getElementById(id) || document.getElementsByName(id)[0];
40
+ if (element) {
41
+ element.scrollIntoView({ behavior: "auto" });
42
+ return true;
43
+ }
44
+ } catch (e) {
45
+ const id = rawHash.replace("#", "");
46
+ const element = document.getElementById(id) || document.getElementsByName(id)[0];
47
+ if (element) {
48
+ element.scrollIntoView({ behavior: "auto" });
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ };
54
+
25
55
  // ====================================================================
26
56
  // 2. PURE HELPERS
27
57
  // ====================================================================
28
58
 
59
+ // Helper to determine active layout folder for a pathname
60
+ const getLayoutKey = (pathname) => {
61
+ const layouts = (typeof window !== "undefined" && window.__DINOU_LAYOUTS__) || ["", "docs"];
62
+ const cleanPath = pathname.split("?")[0].split("#")[0];
63
+ const segments = cleanPath.split("/").filter(Boolean);
64
+
65
+ for (let i = segments.length; i >= 1; i--) {
66
+ const candidateSegments = segments.slice(0, i);
67
+ const candidate = candidateSegments.join("/");
68
+ if (layouts.includes(candidate)) {
69
+ return "/" + candidate;
70
+ }
71
+ const dynamicMatch = layouts.find((l) => {
72
+ const lSegs = l
73
+ .split("/")
74
+ .filter(Boolean)
75
+ .filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")));
76
+ if (lSegs.length !== candidateSegments.length) return false;
77
+ return lSegs.every((seg, idx) => {
78
+ if (seg.startsWith("[") && seg.endsWith("]")) return true;
79
+ return seg === candidateSegments[idx];
80
+ });
81
+ });
82
+ if (dynamicMatch) {
83
+ return "/" + candidate;
84
+ }
85
+ }
86
+ if (
87
+ layouts.includes("") ||
88
+ layouts.includes("/") ||
89
+ layouts.some(
90
+ (l) =>
91
+ l
92
+ .split("/")
93
+ .filter(Boolean)
94
+ .filter((s) => !(s.startsWith("(") && s.endsWith(")"))).length === 0
95
+ )
96
+ ) {
97
+ return "/";
98
+ }
99
+ return null;
100
+ };
101
+
29
102
  // Helper to detect if we only change the hash on the same page
30
103
  const isHashChangeOnly = (finalPath) => {
31
104
  const targetUrl = new URL(finalPath, window.location.origin);
@@ -41,43 +114,35 @@ const isHashChangeOnly = (finalPath) => {
41
114
  );
42
115
  };
43
116
 
44
- const getRSCPayload = (rscKey, isPrefetch = false) => {
45
- const url = rscKey.split("::")[0];
46
- // 1. Check Idempotence (Avoids the infinite loop of React)
47
- if (cache.has(url)) {
48
- return cache.get(url);
117
+ const getPagePayload = (route, isPrefetch = false) => {
118
+ const url = route.split("::")[0];
119
+ if (pageCache.has(url)) {
120
+ return pageCache.get(url);
49
121
  }
50
122
 
51
- // 2. Global Flags Logic (Only first time)
52
123
  let payloadUrl;
53
124
  if (window.__DINOU_USE_OLD_RSC__ || window.__DINOU_USE_STATIC__) {
54
125
  payloadUrl = window.__DINOU_USE_OLD_RSC__
55
126
  ? window.__DINOU_USE_STATIC__
56
- ? "/____rsc_payload_old_static____" + url
57
- : "/____rsc_payload_old____" + url
127
+ ? "/____rsc_page_old_static____" + url
128
+ : "/____rsc_page_old____" + url
58
129
  : window.__DINOU_USE_STATIC__
59
- ? "/____rsc_payload_static____" + url
60
- : "/____rsc_payload____" + url;
61
-
62
- // Clean flags immediately
63
- window.__DINOU_USE_OLD_RSC__ = false;
64
- window.__DINOU_USE_STATIC__ = false;
130
+ ? "/____rsc_page_static____" + url
131
+ : "/____rsc_page____" + url;
65
132
  } else {
66
- payloadUrl = "/____rsc_payload____" + url;
133
+ payloadUrl = "/____rsc_page____" + url;
67
134
  }
68
135
 
69
136
  const buildId = window.__DINOU_BUILD_ID__;
70
137
  if (buildId) {
71
138
  payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
72
- window.__DINOU_BUILD_ID__ = undefined;
73
139
  }
74
140
 
75
- // 3. Fetch and Cache Storage
76
141
  const promise = createFromFetch(
77
142
  fetch(payloadUrl).then((res) => {
78
143
  if (res.headers.has("x-rsc-redirect")) {
79
144
  const redirectUrl = res.headers.get("x-rsc-redirect");
80
- cache.delete(url);
145
+ pageCache.delete(url);
81
146
  if (!isPrefetch) {
82
147
  if (window.__DINOU_ROUTER_NAVIGATE__) {
83
148
  window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
@@ -85,7 +150,6 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
85
150
  window.location.href = redirectUrl;
86
151
  }
87
152
  }
88
- // Return a promise that never resolves to avoid React Server DOM throwing "Connection closed"
89
153
  return new Promise(() => {});
90
154
  }
91
155
  return res;
@@ -94,18 +158,59 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
94
158
  callServer: async (id, args) => {
95
159
  const proxy = createServerFunctionProxy(id);
96
160
  return proxy(...args);
161
+ },
162
+ }
163
+ );
164
+
165
+ promise.catch(() => {});
166
+ pageCache.set(url, promise);
167
+ return promise;
168
+ };
169
+
170
+ const getLayoutPayload = (layoutKey, searchPart = "", version = 0) => {
171
+ if (!layoutKey) return null;
172
+ const cacheKey = `${layoutKey}${searchPart}`;
173
+ if (layoutCache.has(cacheKey)) {
174
+ return layoutCache.get(cacheKey);
175
+ }
176
+
177
+ const cleanKey = layoutKey === "/" ? "" : layoutKey;
178
+ let layoutUrl =
179
+ (window.__DINOU_USE_STATIC__
180
+ ? "/____rsc_layout_static____"
181
+ : "/____rsc_layout____") + cleanKey + searchPart;
182
+
183
+ if (version) {
184
+ layoutUrl += (layoutUrl.includes("?") ? "&" : "?") + "v=" + version;
185
+ }
186
+
187
+ const promise = createFromFetch(
188
+ fetch(layoutUrl).then((res) => {
189
+ if (!res.ok) {
190
+ console.warn(
191
+ `[Dinou Router] Layout ${layoutKey} fetch returned status ${res.status}`
192
+ );
97
193
  }
194
+ return res;
195
+ }),
196
+ {
197
+ callServer: async (id, args) => {
198
+ const proxy = createServerFunctionProxy(id);
199
+ return proxy(...args);
200
+ },
98
201
  }
99
202
  );
100
- cache.set(url, promise); // <--- KEY TO AVOID LOOP
203
+
204
+ promise.catch(() => {});
205
+ layoutCache.set(cacheKey, promise);
101
206
  return promise;
102
207
  };
103
208
 
104
209
  const getErrorRSCPayload = (route, error) => {
105
210
  const url = route.split("::")[0];
106
211
  const cacheKey = `error::${url}::${error.message || String(error)}`;
107
- if (cache.has(cacheKey)) {
108
- return cache.get(cacheKey);
212
+ if (pageCache.has(cacheKey)) {
213
+ return pageCache.get(cacheKey);
109
214
  }
110
215
 
111
216
  const payloadUrl = "/____rsc_payload_error____" + url;
@@ -125,13 +230,13 @@ const getErrorRSCPayload = (route, error) => {
125
230
  }).then((res) => {
126
231
  if (res.headers.has("x-rsc-redirect")) {
127
232
  const redirectUrl = res.headers.get("x-rsc-redirect");
128
- cache.delete(cacheKey);
233
+ pageCache.delete(cacheKey);
129
234
  if (window.__DINOU_ROUTER_NAVIGATE__) {
130
235
  window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
131
236
  } else {
132
237
  window.location.href = redirectUrl;
133
238
  }
134
- return new Promise(() => {});
239
+ return new Promise(() => { });
135
240
  }
136
241
  return res;
137
242
  }),
@@ -142,10 +247,43 @@ const getErrorRSCPayload = (route, error) => {
142
247
  }
143
248
  }
144
249
  );
145
- cache.set(cacheKey, promise);
250
+ pageCache.set(cacheKey, promise);
146
251
  return promise;
147
252
  };
148
253
 
254
+ const fetchSlotRSC = (slotId, options = {}) => {
255
+ const currentPath = typeof window !== "undefined" ? window.location.pathname : "/";
256
+ const freshQuery = options && options.fresh ? "&fresh=1" : "";
257
+ const slotUrl = `/____rsc_slot____/${slotId}?t=${Date.now()}${freshQuery}`;
258
+ return createFromFetch(
259
+ fetch(slotUrl, {
260
+ headers: {
261
+ "x-dinou-current-path": currentPath,
262
+ },
263
+ }),
264
+ {
265
+ callServer: async (id, args) => {
266
+ const proxy = createServerFunctionProxy(id);
267
+ return proxy(...args);
268
+ },
269
+ }
270
+ );
271
+ };
272
+
273
+ export const refreshSlot = (slotId, options) => {
274
+ if (typeof window !== "undefined") {
275
+ window.dispatchEvent(
276
+ new CustomEvent("dinou:refresh-slot", { detail: { id: slotId, options } })
277
+ );
278
+ }
279
+ };
280
+
281
+ if (typeof globalThis !== "undefined") {
282
+ globalThis.__DINOU_GET_ERROR_RSC_PAYLOAD__ = getErrorRSCPayload;
283
+ globalThis.__DINOU_FETCH_SLOT__ = fetchSlotRSC;
284
+ globalThis.__DINOU_REFRESH_SLOT__ = refreshSlot;
285
+ }
286
+
149
287
  class ErrorBoundary extends Component {
150
288
  constructor(props) {
151
289
  super(props);
@@ -163,6 +301,12 @@ class ErrorBoundary extends Component {
163
301
  }
164
302
  }
165
303
 
304
+ componentDidUpdate(prevProps) {
305
+ if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
306
+ this.setState({ hasError: false, error: null });
307
+ }
308
+ }
309
+
166
310
  render() {
167
311
  if (this.state.hasError) {
168
312
  if (this.props.fallback) {
@@ -170,22 +314,78 @@ class ErrorBoundary extends Component {
170
314
  }
171
315
  const isDev = process.env.NODE_ENV !== "production";
172
316
  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>
317
+ <html lang="en">
318
+ <head>
319
+ <meta charSet="UTF-8" />
320
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
321
+ <title>"Dinou Dev Error from client.jsx ErrorBoundary"</title>
322
+ </head>
323
+ <body
324
+ style={{
325
+ margin: 0,
326
+ backgroundColor: "#fff1f2",
327
+ padding: "20px",
328
+ boxSizing: "border-box",
329
+ }}
330
+ >
331
+ <div
332
+ style={{
333
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
334
+ padding: "32px",
335
+ maxWidth: "960px",
336
+ margin: "40px auto",
337
+ backgroundColor: "#fff1f2",
338
+ border: "1px solid #fecdd3",
339
+ borderRadius: "12px",
340
+ color: "#9f1239",
341
+ boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.1)",
342
+ }}
343
+ >
344
+ <div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "12px" }}>
345
+ <span
346
+ style={{
347
+ background: "#e11d48",
348
+ color: "white",
349
+ padding: "2px 8px",
350
+ borderRadius: "9999px",
351
+ fontSize: "12px",
352
+ fontWeight: "bold",
353
+ }}
354
+ >
355
+ {isDev ? "Dinou Dev Error" : "Application Error"}
356
+ </span>
357
+ <h2 style={{ margin: 0, fontSize: "1.25rem", fontWeight: 700, color: "#881337" }}>
358
+ {this.state.error?.message || "Unhandled Application Error"}
359
+ </h2>
360
+ </div>
361
+ {isDev && (
362
+ <p style={{ margin: "0 0 16px 0", fontSize: "0.875rem", color: "#9f1239", lineHeight: 1.5 }}>
363
+ An unhandled error occurred during rendering. You can provide a custom error UI by creating an{" "}
364
+ <code style={{ background: "#ffe4e6", padding: "2px 6px", borderRadius: "4px", fontWeight: 600 }}>
365
+ error.tsx
366
+ </code>{" "}
367
+ file in your route folder.
368
+ </p>
369
+ )}
370
+ {isDev && this.state.error?.stack && (
371
+ <pre
372
+ style={{
373
+ background: "#0f172a",
374
+ color: "#f8fafc",
375
+ padding: "16px",
376
+ borderRadius: "8px",
377
+ overflowX: "auto",
378
+ fontSize: "0.8125rem",
379
+ lineHeight: 1.6,
380
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
381
+ }}
382
+ >
383
+ {cleanErrorStack(this.state.error.stack)}
384
+ </pre>
385
+ )}
386
+ </div>
387
+ </body>
388
+ </html>
189
389
  );
190
390
  }
191
391
  return this.props.children;
@@ -202,10 +402,32 @@ function Router() {
202
402
  const [isPopState, setIsPopState] = useState(false);
203
403
  const [isPending, startTransition] = useTransition();
204
404
  const [version, setVersion] = useState(0);
405
+ const [layoutVersion, setLayoutVersion] = useState(0);
205
406
  const [navError, setNavError] = useState(null);
407
+ const [navCount, setNavCount] = useState(0);
408
+
409
+ // If the initial SSR render had a page-level error wrapped inside a working layout,
410
+ // we hydrate the layout normally so the navbar/sidebar are fully interactive,
411
+ // while supplying the server error directly to DinouPageSlot to avoid refetching.
412
+ const initialPageErrorRef = useRef(
413
+ typeof window !== "undefined" &&
414
+ window.__DINOU_ERROR_MESSAGE__ &&
415
+ window.__DINOU_ERROR_SEGMENT__ === "page"
416
+ ? (() => {
417
+ const err = new Error(window.__DINOU_ERROR_MESSAGE__);
418
+ err.name = window.__DINOU_ERROR_NAME__ || "Error";
419
+ if (window.__DINOU_ERROR_STACK__) err.stack = window.__DINOU_ERROR_STACK__;
420
+ return getErrorRSCPayload(getCurrentRoute(), err);
421
+ })()
422
+ : null
423
+ );
206
424
 
207
425
  // 🧭 NAVIGATE FUNCTION (Core Logic)
208
426
  const navigate = (href, options = {}) => {
427
+ initialPageErrorRef.current = null;
428
+ if (typeof window !== "undefined" && window.__DINOU_ACTIVE_LAYOUT__) {
429
+ window.__DINOU_ACTIVE_LAYOUT__ = null;
430
+ }
209
431
  const finalPath = resolveUrl(href, window.location.pathname);
210
432
 
211
433
  // 🛡️ NAVIGATE PROTECTION: Hash Detection
@@ -227,8 +449,7 @@ function Router() {
227
449
  }
228
450
 
229
451
  if (options.fresh) {
230
- // console.log(`[Router] Force refreshing: ${finalPath}`);
231
- cache.delete(finalPath);
452
+ pageCache.delete(finalPath);
232
453
  }
233
454
 
234
455
  // Normal RSC Navigation
@@ -236,7 +457,6 @@ function Router() {
236
457
  window.location.pathname + window.location.search,
237
458
  window.scrollY,
238
459
  );
239
- // cache.delete(finalPath);
240
460
  if (options.replace) {
241
461
  window.history.replaceState(null, "", finalPath);
242
462
  } else {
@@ -247,18 +467,52 @@ function Router() {
247
467
  setIsPopState(false);
248
468
  setRoute(finalPath);
249
469
  setNavError(null);
470
+ setNavCount((c) => c + 1);
250
471
  });
251
472
  };
252
473
 
474
+ const back = () => window.history.back();
475
+ const forward = () => window.history.forward();
476
+ const refresh = useCallback((options = {}) => {
477
+ const currentPath = window.location.pathname + window.location.search;
478
+ pageCache.delete(currentPath);
479
+
480
+ const shouldRefreshLayout = Boolean(options && options.layout);
481
+ if (shouldRefreshLayout) {
482
+ const currentLayoutKey = getLayoutKey(window.location.pathname);
483
+ if (currentLayoutKey) {
484
+ for (const k of layoutCache.keys()) {
485
+ if (k === currentLayoutKey || k.startsWith(currentLayoutKey + "?")) {
486
+ layoutCache.delete(k);
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ startTransition(() => {
493
+ setVersion((v) => v + 1);
494
+ if (shouldRefreshLayout) {
495
+ setLayoutVersion((v) => v + 1);
496
+ }
497
+ setNavError(null);
498
+ setNavCount((c) => c + 1);
499
+ });
500
+ }, [startTransition]);
501
+
253
502
  // 🔌 EFFECT 1: Expose Global Prefetch & Navigation
254
503
  useEffect(() => {
255
504
  window.__DINOU_PREFETCH__ = (url) => {
256
505
  // 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
257
506
  if (isHashChangeOnly(url)) return;
258
- getRSCPayload(url, true);
507
+ const lKey = getLayoutKey(url);
508
+ if (lKey && !layoutCache.has(lKey)) {
509
+ getLayoutPayload(lKey);
510
+ }
511
+ getPagePayload(url, true);
259
512
  };
260
513
 
261
514
  window.__DINOU_ROUTER_NAVIGATE__ = navigate;
515
+ window.__DINOU_ROUTER_REFRESH__ = refresh;
262
516
 
263
517
  // Hydration
264
518
  document.body.setAttribute("data-hydrated", "true");
@@ -267,25 +521,11 @@ function Router() {
267
521
  if (window.__DINOU_ROUTER_NAVIGATE__ === navigate) {
268
522
  window.__DINOU_ROUTER_NAVIGATE__ = undefined;
269
523
  }
524
+ if (window.__DINOU_ROUTER_REFRESH__ === refresh) {
525
+ window.__DINOU_ROUTER_REFRESH__ = undefined;
526
+ }
270
527
  };
271
- }, [navigate]);
272
-
273
- const back = () => window.history.back();
274
- const forward = () => window.history.forward();
275
- const refresh = () => {
276
- const currentPath = window.location.pathname + window.location.search;
277
- // console.log(`[Router] Soft Refreshing: ${currentPath}`);
278
-
279
- // 1. Delete cache to ensure fresh data
280
- cache.delete(currentPath);
281
-
282
- // 2. Start transition (to show isPending if you want)
283
- startTransition(() => {
284
- // 3. Increment version to force re-execution of useMemo
285
- setVersion((v) => v + 1);
286
- setNavError(null);
287
- });
288
- };
528
+ }, [navigate, refresh]);
289
529
 
290
530
  // 🔌 EFFECT 2: Global Listeners (Click and PopState)
291
531
  useEffect(() => {
@@ -325,23 +565,68 @@ function Router() {
325
565
  };
326
566
 
327
567
  const onPopState = () => {
568
+ initialPageErrorRef.current = null;
569
+ if (typeof window !== "undefined" && window.__DINOU_ACTIVE_LAYOUT__) {
570
+ window.__DINOU_ACTIVE_LAYOUT__ = null;
571
+ }
328
572
  const target = getCurrentRoute();
329
- // Optional: cache.delete(target); // Uncomment if you want refresh on going back
330
573
  startTransition(() => {
331
574
  setIsPopState(true);
332
575
  setRoute(target);
333
576
  setNavError(null);
577
+ setNavCount((c) => c + 1);
334
578
  });
579
+
580
+ if (window.location.hash) {
581
+ requestAnimationFrame(() => {
582
+ if (!scrollToCurrentHash()) {
583
+ let frames = 0;
584
+ const interval = setInterval(() => {
585
+ frames++;
586
+ if (scrollToCurrentHash() || frames > 20) {
587
+ clearInterval(interval);
588
+ }
589
+ }, 50);
590
+ }
591
+ });
592
+ } else if (target === route) {
593
+ requestAnimationFrame(() => {
594
+ const savedY = scrollCache.get(target);
595
+ if (savedY !== undefined) {
596
+ window.scrollTo(0, savedY);
597
+ } else {
598
+ window.scrollTo(0, 0);
599
+ }
600
+ });
601
+ }
602
+ };
603
+
604
+ const onHashChange = () => {
605
+ if (window.location.hash) {
606
+ requestAnimationFrame(() => {
607
+ if (!scrollToCurrentHash()) {
608
+ let frames = 0;
609
+ const interval = setInterval(() => {
610
+ frames++;
611
+ if (scrollToCurrentHash() || frames > 20) {
612
+ clearInterval(interval);
613
+ }
614
+ }, 50);
615
+ }
616
+ });
617
+ }
335
618
  };
336
619
 
337
620
  window.addEventListener("click", onNavigate);
338
621
  window.addEventListener("popstate", onPopState);
622
+ window.addEventListener("hashchange", onHashChange);
339
623
 
340
624
  return () => {
341
625
  window.removeEventListener("click", onNavigate);
342
626
  window.removeEventListener("popstate", onPopState);
627
+ window.removeEventListener("hashchange", onHashChange);
343
628
  };
344
- }, []);
629
+ }, [route]);
345
630
 
346
631
  // 🔌 EFFECT 3: Scroll Management (Restoration)
347
632
  useLayoutEffect(() => {
@@ -362,21 +647,47 @@ function Router() {
362
647
 
363
648
  // 🔌 EFFECT 4: Scroll Management (Hash on new page)
364
649
  useEffect(() => {
365
- const hash = window.location.hash;
366
- if (!hash) return;
367
-
368
- requestAnimationFrame(() => {
369
- const id = hash.replace("#", "");
370
- const element = document.getElementById(id);
371
- if (element) {
372
- element.scrollIntoView({ behavior: "auto" });
650
+ if (!window.location.hash) return;
651
+ if (scrollToCurrentHash()) return;
652
+
653
+ let frames = 0;
654
+ const interval = setInterval(() => {
655
+ frames++;
656
+ if (scrollToCurrentHash() || frames > 20) {
657
+ clearInterval(interval);
373
658
  }
374
- });
659
+ }, 50);
660
+
661
+ return () => clearInterval(interval);
375
662
  }, [route]);
376
663
 
377
- // RSC Logic
378
- const rscKey = route + "::" + version;
379
- const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
664
+ // RSC Segmented Navigation Logic
665
+ const cleanRoutePath = route.split("?")[0].split("#")[0];
666
+ const searchPart = route.includes("?") ? "?" + route.split("?")[1].split("#")[0] : "";
667
+ const layoutKey = useMemo(() => {
668
+ if (
669
+ typeof window !== "undefined" &&
670
+ window.__DINOU_ACTIVE_LAYOUT__ &&
671
+ route === getCurrentRoute()
672
+ ) {
673
+ return window.__DINOU_ACTIVE_LAYOUT__;
674
+ }
675
+ return getLayoutKey(cleanRoutePath);
676
+ }, [cleanRoutePath, route]);
677
+
678
+ const pagePromise = useMemo(() => {
679
+ if (initialPageErrorRef.current && route === getCurrentRoute()) {
680
+ return initialPageErrorRef.current;
681
+ }
682
+ if (navError) return getErrorRSCPayload(route, navError);
683
+ return getPagePayload(route + (version ? `?v=${version}` : ""));
684
+ }, [route, version, navError]);
685
+
686
+ const layoutPromise = useMemo(() => {
687
+ return layoutKey !== null
688
+ ? getLayoutPayload(layoutKey, searchPart, layoutVersion)
689
+ : null;
690
+ }, [layoutKey, searchPart, layoutVersion]);
380
691
 
381
692
  const contextValue = useMemo(
382
693
  () => ({
@@ -387,22 +698,81 @@ function Router() {
387
698
  refresh,
388
699
  isPending,
389
700
  }),
390
- [route, isPending],
701
+ [route, isPending, refresh],
702
+ );
703
+
704
+ const slotContextValue = useMemo(
705
+ () => ({
706
+ pagePromise,
707
+ resetKey: `${route}::${navCount}`,
708
+ }),
709
+ [pagePromise, route, navCount]
391
710
  );
392
711
 
393
712
  return (
394
713
  <RouterContext.Provider value={contextValue}>
395
714
  <ErrorBoundary
396
- key={navError ? "error" : "normal"}
715
+ resetKey={`${route}::${navCount}`}
397
716
  onError={setNavError}
398
717
  >
399
- {use(content)}
718
+ <DinouPageContext.Provider value={slotContextValue}>
719
+ {navError
720
+ ? use(pagePromise)
721
+ : layoutPromise
722
+ ? use(layoutPromise)
723
+ : use(pagePromise)}
724
+ </DinouPageContext.Provider>
400
725
  </ErrorBoundary>
401
726
  </RouterContext.Provider>
402
727
  );
403
728
  }
404
729
 
405
- hydrateRoot(document, <Router />);
730
+ const routerElement = createElement(Router);
731
+ const isDev = process.env.NODE_ENV !== "production";
732
+ const useStrictMode = isDev && (typeof window === "undefined" || window.__DINOU_STRICT_MODE__ !== false);
733
+
734
+ const app = useStrictMode
735
+ ? createElement(StrictMode, null, routerElement)
736
+ : routerElement;
737
+
738
+ const onRecoverableError = (error) => {
739
+ // 🛡️ Filter benign ViewTransition aborts when document is hidden (e.g. background tab or HMR while in IDE)
740
+ if (
741
+ error?.name === "InvalidStateError" &&
742
+ (error?.message?.includes("Document hidden") || (typeof document !== "undefined" && document.hidden))
743
+ ) {
744
+ return;
745
+ }
746
+ if (error?.name === "AbortError" && error?.message?.includes("transition")) {
747
+ return;
748
+ }
749
+
750
+ if (typeof reportError === "function") {
751
+ reportError(error);
752
+ } else {
753
+ console.error(error);
754
+ }
755
+ };
756
+
757
+ // If the server rendered a fatal layout error during SSR (meaning there is no layout DOM to hydrate),
758
+ // do NOT attempt to hydrate the application over it.
759
+ const hasInitialServerError =
760
+ typeof window !== "undefined" && Boolean(window.__DINOU_ERROR_MESSAGE__);
761
+ const isLayoutError =
762
+ hasInitialServerError && window.__DINOU_ERROR_SEGMENT__ !== "page";
763
+
764
+ if (!isLayoutError) {
765
+ hydrateRoot(document, app, { onRecoverableError });
766
+ } else {
767
+ console.warn(
768
+ "[Dinou] Initial layout server error detected:",
769
+ window.__DINOU_ERROR_MESSAGE__,
770
+ "- skipping client hydration to preserve the server-rendered error screen."
771
+ );
772
+ if (typeof document !== "undefined" && document.body) {
773
+ document.body.setAttribute("data-hydrated", "true");
774
+ }
775
+ }
406
776
 
407
777
  if (import.meta.hot) {
408
778
  import.meta.hot.accept();