dinou 6.1.1 → 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 +94 -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 +16 -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 -108
  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,30 +1,104 @@
1
+ // dinou/core/client-webpack.jsx
2
+ if (typeof window !== "undefined") {
3
+ if (!window.$RefreshReg$) window.$RefreshReg$ = () => { };
4
+ if (!window.$RefreshSig$) window.$RefreshSig$ = () => (type) => type;
5
+ }
1
6
  import {
2
7
  use,
8
+ useRef,
3
9
  useState,
4
10
  useEffect,
5
11
  useTransition,
6
12
  useLayoutEffect,
7
13
  useMemo,
14
+ useCallback,
8
15
  Component,
16
+ createElement,
17
+ StrictMode,
9
18
  } from "react";
10
19
  import { createFromFetch } from "react-server-dom-webpack/client";
11
20
  import { hydrateRoot } from "react-dom/client";
12
21
  import { RouterContext } from "./navigation.js";
13
22
  import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
14
23
  import { createServerFunctionProxy } from "./server-function-proxy-webpack.js";
24
+ import { DinouPageContext, DinouPageSlot, cleanErrorStack } from "./slot.js";
15
25
 
16
26
  // ====================================================================
17
27
  // 1. GLOBAL STATE (Outside the component)
18
28
  // ====================================================================
19
- const cache = new Map();
29
+ const pageCache = new Map();
30
+ const layoutCache = new Map();
20
31
  const scrollCache = new Map();
21
32
 
22
33
  const getCurrentRoute = () => window.location.pathname + window.location.search;
23
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
+
24
55
  // ====================================================================
25
56
  // 2. PURE HELPERS
26
57
  // ====================================================================
27
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
+
28
102
  // Helper to detect if we only change the hash on the same page
29
103
  const isHashChangeOnly = (finalPath) => {
30
104
  const targetUrl = new URL(finalPath, window.location.origin);
@@ -40,37 +114,35 @@ const isHashChangeOnly = (finalPath) => {
40
114
  );
41
115
  };
42
116
 
43
- const getRSCPayload = (rscKey, isPrefetch = false) => {
44
- const url = rscKey.split("::")[0];
45
- // 1. Check Idempotence (Avoids the infinite loop of React)
46
- if (cache.has(url)) {
47
- 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);
48
121
  }
49
122
 
50
- // 2. Global Flags Logic (Only first time)
51
123
  let payloadUrl;
52
124
  if (window.__DINOU_USE_OLD_RSC__ || window.__DINOU_USE_STATIC__) {
53
125
  payloadUrl = window.__DINOU_USE_OLD_RSC__
54
126
  ? window.__DINOU_USE_STATIC__
55
- ? "/____rsc_payload_old_static____" + url
56
- : "/____rsc_payload_old____" + url
127
+ ? "/____rsc_page_old_static____" + url
128
+ : "/____rsc_page_old____" + url
57
129
  : window.__DINOU_USE_STATIC__
58
- ? "/____rsc_payload_static____" + url
59
- : "/____rsc_payload____" + url;
60
-
61
- // Clean flags immediately
62
- window.__DINOU_USE_OLD_RSC__ = false;
63
- window.__DINOU_USE_STATIC__ = false;
130
+ ? "/____rsc_page_static____" + url
131
+ : "/____rsc_page____" + url;
64
132
  } else {
65
- payloadUrl = "/____rsc_payload____" + url;
133
+ payloadUrl = "/____rsc_page____" + url;
134
+ }
135
+
136
+ const buildId = window.__DINOU_BUILD_ID__;
137
+ if (buildId) {
138
+ payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
66
139
  }
67
140
 
68
- // 3. Fetch and Cache Storage
69
141
  const promise = createFromFetch(
70
142
  fetch(payloadUrl).then((res) => {
71
143
  if (res.headers.has("x-rsc-redirect")) {
72
144
  const redirectUrl = res.headers.get("x-rsc-redirect");
73
- cache.delete(url);
145
+ pageCache.delete(url);
74
146
  if (!isPrefetch) {
75
147
  if (window.__DINOU_ROUTER_NAVIGATE__) {
76
148
  window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
@@ -78,7 +150,6 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
78
150
  window.location.href = redirectUrl;
79
151
  }
80
152
  }
81
- // Return a promise that never resolves to avoid React Server DOM throwing "Connection closed"
82
153
  return new Promise(() => {});
83
154
  }
84
155
  return res;
@@ -87,18 +158,59 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
87
158
  callServer: async (id, args) => {
88
159
  const proxy = createServerFunctionProxy(id);
89
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
+ );
90
193
  }
194
+ return res;
195
+ }),
196
+ {
197
+ callServer: async (id, args) => {
198
+ const proxy = createServerFunctionProxy(id);
199
+ return proxy(...args);
200
+ },
91
201
  }
92
202
  );
93
- cache.set(url, promise); // <--- KEY TO AVOID LOOP
203
+
204
+ promise.catch(() => {});
205
+ layoutCache.set(cacheKey, promise);
94
206
  return promise;
95
207
  };
96
208
 
97
209
  const getErrorRSCPayload = (route, error) => {
98
210
  const url = route.split("::")[0];
99
211
  const cacheKey = `error::${url}::${error.message || String(error)}`;
100
- if (cache.has(cacheKey)) {
101
- return cache.get(cacheKey);
212
+ if (pageCache.has(cacheKey)) {
213
+ return pageCache.get(cacheKey);
102
214
  }
103
215
 
104
216
  const payloadUrl = "/____rsc_payload_error____" + url;
@@ -118,7 +230,7 @@ const getErrorRSCPayload = (route, error) => {
118
230
  }).then((res) => {
119
231
  if (res.headers.has("x-rsc-redirect")) {
120
232
  const redirectUrl = res.headers.get("x-rsc-redirect");
121
- cache.delete(cacheKey);
233
+ pageCache.delete(cacheKey);
122
234
  if (window.__DINOU_ROUTER_NAVIGATE__) {
123
235
  window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
124
236
  } else {
@@ -132,13 +244,46 @@ const getErrorRSCPayload = (route, error) => {
132
244
  callServer: async (id, args) => {
133
245
  const proxy = createServerFunctionProxy(id);
134
246
  return proxy(...args);
135
- }
247
+ },
136
248
  }
137
249
  );
138
- cache.set(cacheKey, promise);
250
+ pageCache.set(cacheKey, promise);
139
251
  return promise;
140
252
  };
141
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
+
142
287
  class ErrorBoundary extends Component {
143
288
  constructor(props) {
144
289
  super(props);
@@ -156,6 +301,12 @@ class ErrorBoundary extends Component {
156
301
  }
157
302
  }
158
303
 
304
+ componentDidUpdate(prevProps) {
305
+ if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
306
+ this.setState({ hasError: false, error: null });
307
+ }
308
+ }
309
+
159
310
  render() {
160
311
  if (this.state.hasError) {
161
312
  if (this.props.fallback) {
@@ -163,29 +314,84 @@ class ErrorBoundary extends Component {
163
314
  }
164
315
  const isDev = process.env.NODE_ENV !== "production";
165
316
  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>
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>
182
389
  );
183
390
  }
184
391
  return this.props.children;
185
392
  }
186
393
  }
187
394
 
188
-
189
395
  // ====================================================================
190
396
  // 3. ROUTER COMPONENT
191
397
  // ====================================================================
@@ -195,10 +401,32 @@ function Router() {
195
401
  const [isPopState, setIsPopState] = useState(false);
196
402
  const [isPending, startTransition] = useTransition();
197
403
  const [version, setVersion] = useState(0);
404
+ const [layoutVersion, setLayoutVersion] = useState(0);
198
405
  const [navError, setNavError] = useState(null);
406
+ const [navCount, setNavCount] = useState(0);
407
+
408
+ // If the initial SSR render had a page-level error wrapped inside a working layout,
409
+ // we hydrate the layout normally so the navbar/sidebar are fully interactive,
410
+ // while supplying the server error directly to DinouPageSlot to avoid refetching.
411
+ const initialPageErrorRef = useRef(
412
+ typeof window !== "undefined" &&
413
+ window.__DINOU_ERROR_MESSAGE__ &&
414
+ window.__DINOU_ERROR_SEGMENT__ === "page"
415
+ ? (() => {
416
+ const err = new Error(window.__DINOU_ERROR_MESSAGE__);
417
+ err.name = window.__DINOU_ERROR_NAME__ || "Error";
418
+ if (window.__DINOU_ERROR_STACK__) err.stack = window.__DINOU_ERROR_STACK__;
419
+ return getErrorRSCPayload(getCurrentRoute(), err);
420
+ })()
421
+ : null
422
+ );
199
423
 
200
424
  // 🧭 NAVIGATE FUNCTION (Core Logic)
201
425
  const navigate = (href, options = {}) => {
426
+ initialPageErrorRef.current = null;
427
+ if (typeof window !== "undefined" && window.__DINOU_ACTIVE_LAYOUT__) {
428
+ window.__DINOU_ACTIVE_LAYOUT__ = null;
429
+ }
202
430
  const finalPath = resolveUrl(href, window.location.pathname);
203
431
 
204
432
  // 🛡️ NAVIGATE PROTECTION: Hash Detection
@@ -220,8 +448,7 @@ function Router() {
220
448
  }
221
449
 
222
450
  if (options.fresh) {
223
- // console.log(`[Router] Force refreshing: ${finalPath}`);
224
- cache.delete(finalPath);
451
+ pageCache.delete(finalPath);
225
452
  }
226
453
 
227
454
  // Normal RSC Navigation
@@ -229,7 +456,6 @@ function Router() {
229
456
  window.location.pathname + window.location.search,
230
457
  window.scrollY,
231
458
  );
232
- // cache.delete(finalPath);
233
459
  if (options.replace) {
234
460
  window.history.replaceState(null, "", finalPath);
235
461
  } else {
@@ -240,18 +466,52 @@ function Router() {
240
466
  setIsPopState(false);
241
467
  setRoute(finalPath);
242
468
  setNavError(null);
469
+ setNavCount((c) => c + 1);
243
470
  });
244
471
  };
245
472
 
473
+ const back = () => window.history.back();
474
+ const forward = () => window.history.forward();
475
+ const refresh = useCallback((options = {}) => {
476
+ const currentPath = window.location.pathname + window.location.search;
477
+ pageCache.delete(currentPath);
478
+
479
+ const shouldRefreshLayout = Boolean(options && options.layout);
480
+ if (shouldRefreshLayout) {
481
+ const currentLayoutKey = getLayoutKey(window.location.pathname);
482
+ if (currentLayoutKey) {
483
+ for (const k of layoutCache.keys()) {
484
+ if (k === currentLayoutKey || k.startsWith(currentLayoutKey + "?")) {
485
+ layoutCache.delete(k);
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ startTransition(() => {
492
+ setVersion((v) => v + 1);
493
+ if (shouldRefreshLayout) {
494
+ setLayoutVersion((v) => v + 1);
495
+ }
496
+ setNavError(null);
497
+ setNavCount((c) => c + 1);
498
+ });
499
+ }, [startTransition]);
500
+
246
501
  // 🔌 EFFECT 1: Expose Global Prefetch & Navigation
247
502
  useEffect(() => {
248
503
  window.__DINOU_PREFETCH__ = (url) => {
249
504
  // 🛡️ PREFETCH PROTECTION: If it's a local hash, do nothing
250
505
  if (isHashChangeOnly(url)) return;
251
- getRSCPayload(url, true);
506
+ const lKey = getLayoutKey(url);
507
+ if (lKey && !layoutCache.has(lKey)) {
508
+ getLayoutPayload(lKey);
509
+ }
510
+ getPagePayload(url, true);
252
511
  };
253
512
 
254
513
  window.__DINOU_ROUTER_NAVIGATE__ = navigate;
514
+ window.__DINOU_ROUTER_REFRESH__ = refresh;
255
515
 
256
516
  // Hydration
257
517
  document.body.setAttribute("data-hydrated", "true");
@@ -260,25 +520,11 @@ function Router() {
260
520
  if (window.__DINOU_ROUTER_NAVIGATE__ === navigate) {
261
521
  window.__DINOU_ROUTER_NAVIGATE__ = undefined;
262
522
  }
523
+ if (window.__DINOU_ROUTER_REFRESH__ === refresh) {
524
+ window.__DINOU_ROUTER_REFRESH__ = undefined;
525
+ }
263
526
  };
264
- }, [navigate]);
265
-
266
- const back = () => window.history.back();
267
- const forward = () => window.history.forward();
268
- const refresh = () => {
269
- const currentPath = window.location.pathname + window.location.search;
270
- // console.log(`[Router] Soft Refreshing: ${currentPath}`);
271
-
272
- // 1. Delete cache to ensure fresh data
273
- cache.delete(currentPath);
274
-
275
- // 2. Start transition (to show isPending if you want)
276
- startTransition(() => {
277
- // 3. Increment version to force re-execution of useMemo
278
- setVersion((v) => v + 1);
279
- setNavError(null);
280
- });
281
- };
527
+ }, [navigate, refresh]);
282
528
 
283
529
  // 🔌 EFFECT 2: Global Listeners (Click and PopState)
284
530
  useEffect(() => {
@@ -318,23 +564,68 @@ function Router() {
318
564
  };
319
565
 
320
566
  const onPopState = () => {
567
+ initialPageErrorRef.current = null;
568
+ if (typeof window !== "undefined" && window.__DINOU_ACTIVE_LAYOUT__) {
569
+ window.__DINOU_ACTIVE_LAYOUT__ = null;
570
+ }
321
571
  const target = getCurrentRoute();
322
- // Optional: cache.delete(target); // Uncomment if you want refresh on going back
323
572
  startTransition(() => {
324
573
  setIsPopState(true);
325
574
  setRoute(target);
326
575
  setNavError(null);
576
+ setNavCount((c) => c + 1);
327
577
  });
578
+
579
+ if (window.location.hash) {
580
+ requestAnimationFrame(() => {
581
+ if (!scrollToCurrentHash()) {
582
+ let frames = 0;
583
+ const interval = setInterval(() => {
584
+ frames++;
585
+ if (scrollToCurrentHash() || frames > 20) {
586
+ clearInterval(interval);
587
+ }
588
+ }, 50);
589
+ }
590
+ });
591
+ } else if (target === route) {
592
+ requestAnimationFrame(() => {
593
+ const savedY = scrollCache.get(target);
594
+ if (savedY !== undefined) {
595
+ window.scrollTo(0, savedY);
596
+ } else {
597
+ window.scrollTo(0, 0);
598
+ }
599
+ });
600
+ }
601
+ };
602
+
603
+ const onHashChange = () => {
604
+ if (window.location.hash) {
605
+ requestAnimationFrame(() => {
606
+ if (!scrollToCurrentHash()) {
607
+ let frames = 0;
608
+ const interval = setInterval(() => {
609
+ frames++;
610
+ if (scrollToCurrentHash() || frames > 20) {
611
+ clearInterval(interval);
612
+ }
613
+ }, 50);
614
+ }
615
+ });
616
+ }
328
617
  };
329
618
 
330
619
  window.addEventListener("click", onNavigate);
331
620
  window.addEventListener("popstate", onPopState);
621
+ window.addEventListener("hashchange", onHashChange);
332
622
 
333
623
  return () => {
334
624
  window.removeEventListener("click", onNavigate);
335
625
  window.removeEventListener("popstate", onPopState);
626
+ window.removeEventListener("hashchange", onHashChange);
336
627
  };
337
- }, []);
628
+ }, [route]);
338
629
 
339
630
  // 🔌 EFFECT 3: Scroll Management (Restoration)
340
631
  useLayoutEffect(() => {
@@ -355,21 +646,47 @@ function Router() {
355
646
 
356
647
  // 🔌 EFFECT 4: Scroll Management (Hash on new page)
357
648
  useEffect(() => {
358
- const hash = window.location.hash;
359
- if (!hash) return;
360
-
361
- requestAnimationFrame(() => {
362
- const id = hash.replace("#", "");
363
- const element = document.getElementById(id);
364
- if (element) {
365
- element.scrollIntoView({ behavior: "auto" });
649
+ if (!window.location.hash) return;
650
+ if (scrollToCurrentHash()) return;
651
+
652
+ let frames = 0;
653
+ const interval = setInterval(() => {
654
+ frames++;
655
+ if (scrollToCurrentHash() || frames > 20) {
656
+ clearInterval(interval);
366
657
  }
367
- });
658
+ }, 50);
659
+
660
+ return () => clearInterval(interval);
368
661
  }, [route]);
369
662
 
370
- // RSC Logic
371
- const rscKey = route + "::" + version;
372
- const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
663
+ // RSC Segmented Navigation Logic
664
+ const cleanRoutePath = route.split("?")[0].split("#")[0];
665
+ const searchPart = route.includes("?") ? "?" + route.split("?")[1].split("#")[0] : "";
666
+ const layoutKey = useMemo(() => {
667
+ if (
668
+ typeof window !== "undefined" &&
669
+ window.__DINOU_ACTIVE_LAYOUT__ &&
670
+ route === getCurrentRoute()
671
+ ) {
672
+ return window.__DINOU_ACTIVE_LAYOUT__;
673
+ }
674
+ return getLayoutKey(cleanRoutePath);
675
+ }, [cleanRoutePath, route]);
676
+
677
+ const pagePromise = useMemo(() => {
678
+ if (initialPageErrorRef.current && route === getCurrentRoute()) {
679
+ return initialPageErrorRef.current;
680
+ }
681
+ if (navError) return getErrorRSCPayload(route, navError);
682
+ return getPagePayload(route + (version ? `?v=${version}` : ""));
683
+ }, [route, version, navError]);
684
+
685
+ const layoutPromise = useMemo(() => {
686
+ return layoutKey !== null
687
+ ? getLayoutPayload(layoutKey, searchPart, layoutVersion)
688
+ : null;
689
+ }, [layoutKey, searchPart, layoutVersion]);
373
690
 
374
691
  const contextValue = useMemo(
375
692
  () => ({
@@ -380,22 +697,80 @@ function Router() {
380
697
  refresh,
381
698
  isPending,
382
699
  }),
383
- [route, isPending],
700
+ [route, isPending, refresh],
701
+ );
702
+
703
+ const slotContextValue = useMemo(
704
+ () => ({
705
+ pagePromise,
706
+ resetKey: `${route}::${navCount}`,
707
+ }),
708
+ [pagePromise, route, navCount]
384
709
  );
385
710
 
386
711
  return (
387
712
  <RouterContext.Provider value={contextValue}>
388
713
  <ErrorBoundary
389
- key={navError ? "error" : "normal"}
714
+ resetKey={`${route}::${navCount}`}
390
715
  onError={setNavError}
391
716
  >
392
- {use(content)}
717
+ <DinouPageContext.Provider value={slotContextValue}>
718
+ {navError
719
+ ? use(pagePromise)
720
+ : layoutPromise
721
+ ? use(layoutPromise)
722
+ : use(pagePromise)}
723
+ </DinouPageContext.Provider>
393
724
  </ErrorBoundary>
394
725
  </RouterContext.Provider>
395
726
  );
396
727
  }
397
728
 
398
- hydrateRoot(document, <Router />);
729
+ const routerElement = createElement(Router);
730
+ const isDev = process.env.NODE_ENV !== "production";
731
+ const useStrictMode = isDev && (typeof window === "undefined" || window.__DINOU_STRICT_MODE__ !== false);
732
+
733
+ const app = useStrictMode
734
+ ? createElement(StrictMode, null, routerElement)
735
+ : routerElement;
736
+
737
+ const onRecoverableError = (error) => {
738
+ if (
739
+ error?.name === "InvalidStateError" &&
740
+ (error?.message?.includes("Document hidden") || (typeof document !== "undefined" && document.hidden))
741
+ ) {
742
+ return;
743
+ }
744
+ if (error?.name === "AbortError" && error?.message?.includes("transition")) {
745
+ return;
746
+ }
747
+
748
+ if (typeof reportError === "function") {
749
+ reportError(error);
750
+ } else {
751
+ console.error(error);
752
+ }
753
+ };
754
+
755
+ // If the server rendered a fatal layout error during SSR (meaning there is no layout DOM to hydrate),
756
+ // do NOT attempt to hydrate the application over it.
757
+ const hasInitialServerError =
758
+ typeof window !== "undefined" && Boolean(window.__DINOU_ERROR_MESSAGE__);
759
+ const isLayoutError =
760
+ hasInitialServerError && window.__DINOU_ERROR_SEGMENT__ !== "page";
761
+
762
+ if (!isLayoutError) {
763
+ hydrateRoot(document, app, { onRecoverableError });
764
+ } else {
765
+ console.warn(
766
+ "[Dinou] Initial layout server error detected:",
767
+ window.__DINOU_ERROR_MESSAGE__,
768
+ "- skipping client hydration to preserve the server-rendered error screen."
769
+ );
770
+ if (typeof document !== "undefined" && document.body) {
771
+ document.body.setAttribute("data-hydrated", "true");
772
+ }
773
+ }
399
774
 
400
775
  if (import.meta.hot) {
401
776
  import.meta.hot.accept();