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.
- package/CHANGELOG.md +99 -0
- package/README.md +9461 -1080
- package/cli.js +392 -169
- package/dinou/adapters/bun.js +62 -0
- package/dinou/adapters/cloudflare.js +65 -0
- package/dinou/adapters/deno.js +48 -0
- package/dinou/adapters/netlify.js +85 -0
- package/dinou/adapters/node.mjs +31 -0
- package/dinou/bun/build.mjs +1077 -0
- package/dinou/cloudflare/build.mjs +1121 -0
- package/dinou/cloudflare/shims/child_process.js +14 -0
- package/dinou/cloudflare/shims/empty.js +2 -0
- package/dinou/cloudflare/shims/fs.js +99 -0
- package/dinou/cloudflare/shims/os.js +29 -0
- package/dinou/config.js +1 -0
- package/dinou/config.mjs +2 -0
- package/dinou/constants.js +29 -4
- package/dinou/core/babel-esm-loader.js +66 -37
- package/dinou/core/bailout-proxy.js +61 -0
- package/dinou/core/build-static-pages.js +309 -157
- package/dinou/core/cache-revalidate.js +471 -144
- package/dinou/core/cache-slot.js +387 -0
- package/dinou/core/client-redirect.jsx +12 -7
- package/dinou/core/client-webpack.jsx +457 -82
- package/dinou/core/client.jsx +452 -82
- package/dinou/core/config.js +11 -0
- package/dinou/core/dinou-paths.js +52 -0
- package/dinou/core/edge-ssr.js +122 -0
- package/dinou/core/export-static.mjs +64 -0
- package/dinou/core/generate-route-types.js +252 -0
- package/dinou/core/generate-static-page.js +55 -94
- package/dinou/core/generate-static-rsc.js +47 -15
- package/dinou/core/generate-static.js +212 -10
- package/dinou/core/generating-isg.js +5 -0
- package/dinou/core/get-asset-from-manifest.js +9 -3
- package/dinou/core/get-error-jsx.js +221 -41
- package/dinou/core/get-file-path-and-dynamic-params.js +9 -13
- package/dinou/core/get-jsx.js +172 -95
- package/dinou/core/handler.js +2296 -0
- package/dinou/core/http-adapter.js +115 -0
- package/dinou/core/import-module.js +83 -53
- package/dinou/core/layout-functions.js +181 -0
- package/dinou/core/link.jsx +18 -6
- package/dinou/core/load-dinou-config.js +60 -0
- package/dinou/core/manifest-provider.js +261 -0
- package/dinou/core/navigation-utils.js +4 -0
- package/dinou/core/navigation.js +50 -39
- package/dinou/core/parse-exports.js +207 -2
- package/dinou/core/ppr-context.js +105 -0
- package/dinou/core/ppr-runtime.js +263 -0
- package/dinou/core/redirect.jsx +20 -19
- package/dinou/core/render-jsx-to-client-jsx.js +133 -15
- package/dinou/core/request-context.js +47 -13
- package/dinou/core/resolve-module-ppr.js +32 -0
- package/dinou/core/revalidating.js +28 -9
- package/dinou/core/route-generator.js +107 -0
- package/dinou/core/rsc-renderer.js +90 -0
- package/dinou/core/run-ssg.js +19 -0
- package/dinou/core/scan-dependency-components.js +218 -0
- package/dinou/core/server-function-proxy-webpack.js +3 -0
- package/dinou/core/server-function-proxy.js +3 -0
- package/dinou/core/slot.js +302 -0
- package/dinou/core/status-manifest.js +41 -1
- package/dinou/core/storage-adapter.js +522 -0
- package/dinou/core/url-resolver.js +16 -16
- package/dinou/core/vfs.js +139 -68
- package/dinou/deno/build.mjs +1112 -0
- package/dinou/deno/seed-kv.js +115 -0
- package/dinou/esbuild/build.mjs +51 -1
- package/dinou/esbuild/dev.mjs +290 -224
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -2
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild.mjs +104 -16
- package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +31 -19
- package/dinou/esbuild/helpers-esbuild/strip-namespace.mjs +9 -0
- package/dinou/esbuild/helpers-esbuild/swc-disk-cache.mjs +225 -0
- package/dinou/esbuild/helpers-esbuild/update-manifest-for-module.mjs +7 -0
- package/dinou/esbuild/helpers-esbuild/write.mjs +111 -2
- package/dinou/esbuild/plugins-esbuild/assets-plugin.mjs +236 -161
- package/dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs +101 -5
- package/dinou/esbuild/plugins-esbuild/react-client-manifest-plugin.mjs +63 -24
- package/dinou/esbuild/plugins-esbuild/server-functions-plugin.mjs +177 -56
- package/dinou/esbuild/plugins-esbuild/skip-missing-entry-points-plugin.mjs +3 -0
- package/dinou/esbuild/plugins-esbuild/stable-chunk-names-and-maps-plugin.mjs +182 -137
- package/dinou/esbuild/plugins-postcss/postcss-extract-plugin.js +38 -11
- package/dinou/esbuild/react-refresh/esm-hmr/client.mjs +55 -8
- package/dinou/esbuild/react-refresh/esm-hmr/server.js +1 -1
- package/dinou/esbuild/react-refresh/esm-hmr-plugin.mjs +238 -101
- package/dinou/esbuild/react-refresh/is-react-refresh-boundary.mjs +18 -4
- package/dinou/index.d.ts +315 -11
- package/dinou/index.js +12 -6
- package/dinou/index.mjs +9 -0
- package/dinou/node/build.mjs +195 -0
- package/dinou/node/bundle-dual-engine.mjs +1012 -0
- package/dinou/node/dev.mjs +2589 -0
- package/dinou/node/port-selector.mjs +164 -0
- package/dinou/node/terminal-status.mjs +345 -0
- package/dinou/package.json +17 -14
- package/dinou/rollup/react-refresh/esm-hmr/client.js +66 -10
- package/dinou/rollup/react-refresh/esm-hmr/server.js +1 -1
- package/dinou/rollup/react-refresh/is-react-refresh-boundary.js +24 -5
- package/dinou/rollup/react-refresh/react-refresh-runtime.js +13 -6
- package/dinou/rollup/react-refresh/react-refresh-wrap-modules.js +56 -11
- package/dinou/rollup/react-refresh/rollup-plugin-esm-hmr.js +176 -48
- package/dinou/rollup/rollup-plugins/dinou-asset-plugin.js +9 -5
- package/dinou/rollup/rollup-plugins/rollup-plugin-memory.js +66 -0
- package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +468 -123
- package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +44 -12
- package/dinou/rollup/rollup-plugins/rollup-plugin-swc.js +100 -0
- package/dinou/rollup/rollup.config.js +155 -43
- package/dinou/server.d.ts +114 -12
- package/dinou/server.js +35 -10
- package/dinou/server.mjs +30 -9
- package/dinou/webpack/helpers/get-webpack-entries.js +14 -19
- package/dinou/webpack/loaders/server-functions-loader.js +22 -1
- package/dinou/webpack/loaders/swc-loader.js +76 -0
- package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -0
- package/dinou/webpack/plugins/server-functions-plugin.js +75 -19
- package/dinou/webpack/plugins/webpack-memory-plugin.js +66 -0
- package/dinou/webpack/webpack.config.js +227 -29
- package/eject.js +173 -94
- package/package.json +110 -107
- package/dinou/core/asset-require-hook.js +0 -58
- package/dinou/core/client-error-webpack.jsx +0 -432
- package/dinou/core/client-error.jsx +0 -432
- package/dinou/core/concurrency-manager.js +0 -48
- package/dinou/core/context-proxy.js +0 -68
- package/dinou/core/generate-static-pages.js +0 -130
- package/dinou/core/generate-static-rscs.js +0 -151
- package/dinou/core/get-ssg-metadata.js +0 -31
- package/dinou/core/register-loader.mjs +0 -11
- package/dinou/core/register-paths.js +0 -27
- package/dinou/core/render-app-to-html.js +0 -430
- package/dinou/core/render-html.js +0 -407
- package/dinou/core/server.js +0 -1542
- package/dinou/esbuild/plugins-esbuild/write-metafile-plugin.mjs +0 -16
package/dinou/core/client.jsx
CHANGED
|
@@ -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
|
|
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
|
|
45
|
-
const url =
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
? "/
|
|
57
|
-
: "/
|
|
127
|
+
? "/____rsc_page_old_static____" + url
|
|
128
|
+
: "/____rsc_page_old____" + url
|
|
58
129
|
: window.__DINOU_USE_STATIC__
|
|
59
|
-
? "/
|
|
60
|
-
: "/
|
|
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 = "/
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
108
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
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
|
|
379
|
-
const
|
|
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
|
-
|
|
715
|
+
resetKey={`${route}::${navCount}`}
|
|
397
716
|
onError={setNavError}
|
|
398
717
|
>
|
|
399
|
-
{
|
|
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
|
-
|
|
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();
|