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
@@ -0,0 +1,387 @@
1
+ // dinou/core/cache-slot.js
2
+ // Primitive for Vertical Segmentation (Micro-ISR) in React Server Components.
3
+ // Caches and invalidates component sub-trees inside pages using revalidate & revalidateTag.
4
+
5
+ const path = require("path");
6
+ const { pathToFileURL } = require("url");
7
+ const React = require("react");
8
+ const { getStorageAdapter } = require("./storage-adapter.js");
9
+ const { asyncRenderJSXToClientJSX } = require("./render-jsx-to-client-jsx.js");
10
+
11
+ const INFLIGHT_KEY = Symbol.for("dinou.slot.inflight.renders");
12
+ if (!globalThis[INFLIGHT_KEY]) {
13
+ globalThis[INFLIGHT_KEY] = new Map();
14
+ }
15
+ const inflightSlotRenders = globalThis[INFLIGHT_KEY];
16
+
17
+ const BG_REVAL_KEY = Symbol.for("dinou.slot.background.revalidations");
18
+ if (!globalThis[BG_REVAL_KEY]) {
19
+ globalThis[BG_REVAL_KEY] = new Set();
20
+ }
21
+ const backgroundRevalidations = globalThis[BG_REVAL_KEY];
22
+
23
+ const SLOT_REGISTRY_KEY = Symbol.for("dinou.slot.registry");
24
+ if (!globalThis[SLOT_REGISTRY_KEY]) {
25
+ globalThis[SLOT_REGISTRY_KEY] = new Map();
26
+ }
27
+ const slotRegistry = globalThis[SLOT_REGISTRY_KEY];
28
+
29
+ let _DinouCacheSlotBoundary = null;
30
+ function getCacheSlotBoundary() {
31
+ if (!_DinouCacheSlotBoundary) {
32
+ let { DinouCacheSlotBoundary } = require("./slot.js");
33
+ if (
34
+ typeof DinouCacheSlotBoundary === "function" &&
35
+ DinouCacheSlotBoundary.$$typeof !== Symbol.for("react.client.reference")
36
+ ) {
37
+ const slotPath = path.resolve(__dirname, "slot.js");
38
+ const fileUrl = pathToFileURL(slotPath).href;
39
+ let regFn = null;
40
+ try {
41
+ const pkg = require("react-server-dom-webpack/server");
42
+ regFn = pkg.registerClientReference;
43
+ } catch (e) {}
44
+ if (!regFn) {
45
+ try {
46
+ const pkg = require("@roggc/react-server-dom-esm/server.node.js");
47
+ regFn = pkg.registerClientReference;
48
+ } catch (e) {}
49
+ }
50
+ if (regFn) {
51
+ DinouCacheSlotBoundary = regFn(
52
+ DinouCacheSlotBoundary,
53
+ fileUrl,
54
+ "DinouCacheSlotBoundary"
55
+ );
56
+ } else {
57
+ Object.defineProperties(DinouCacheSlotBoundary, {
58
+ $$typeof: { value: Symbol.for("react.client.reference") },
59
+ $$id: { value: fileUrl + "#DinouCacheSlotBoundary" },
60
+ });
61
+ }
62
+ }
63
+ _DinouCacheSlotBoundary = DinouCacheSlotBoundary;
64
+ }
65
+ return _DinouCacheSlotBoundary;
66
+ }
67
+
68
+ /**
69
+ * Serializes a resolved JSX element tree into a JSON-safe string,
70
+ * preserving symbols, React 19 transitional elements, and client component references.
71
+ */
72
+ function serializeJSX(jsx) {
73
+ return JSON.stringify(jsx, (key, value) => {
74
+ if (typeof value === "symbol") {
75
+ return { __dinou_symbol__: Symbol.keyFor(value) || value.description };
76
+ }
77
+ if (
78
+ (typeof value === "function" || typeof value === "object") &&
79
+ value !== null &&
80
+ value.$$typeof === Symbol.for("react.client.reference")
81
+ ) {
82
+ return {
83
+ __dinou_client_ref__: true,
84
+ id: value.$$id || value.name,
85
+ name: value.name,
86
+ async: value.$$async,
87
+ };
88
+ }
89
+ if (
90
+ value &&
91
+ typeof value === "object" &&
92
+ value.$$typeof === Symbol.for("react.transitional.element")
93
+ ) {
94
+ return {
95
+ __dinou_element__: true,
96
+ type: value.type,
97
+ props: value.props,
98
+ key: value.key,
99
+ };
100
+ }
101
+ return value;
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Deserializes a JSON string back into a genuine React 19 element tree.
107
+ */
108
+ function deserializeJSX(json) {
109
+ const parsed = JSON.parse(json, (key, value) => {
110
+ if (value && typeof value === "object") {
111
+ if (value.__dinou_symbol__) {
112
+ return Symbol.for(value.__dinou_symbol__);
113
+ }
114
+ if (value.__dinou_client_ref__) {
115
+ const fakeRef = function () {};
116
+ Object.defineProperties(fakeRef, {
117
+ $$typeof: { value: Symbol.for("react.client.reference") },
118
+ $$id: { value: value.id },
119
+ name: { value: value.name },
120
+ $$async: { value: value.async },
121
+ });
122
+ return fakeRef;
123
+ }
124
+ }
125
+ return value;
126
+ });
127
+
128
+ function toReactElement(node) {
129
+ if (node == null || typeof node !== "object") return node;
130
+ if (Array.isArray(node)) return node.map(toReactElement);
131
+
132
+ if (node.__dinou_element__) {
133
+ const type = node.type;
134
+ const props = {};
135
+ if (node.props) {
136
+ for (const [k, v] of Object.entries(node.props)) {
137
+ if (k === "key") continue;
138
+ props[k] = toReactElement(v);
139
+ }
140
+ }
141
+ if (node.key != null) {
142
+ props.key = node.key;
143
+ }
144
+ const children = props.children;
145
+ if (Array.isArray(children)) {
146
+ delete props.children;
147
+ return React.createElement(type, props, ...children);
148
+ }
149
+ return React.createElement(type, props);
150
+ }
151
+ return node;
152
+ }
153
+
154
+ return toReactElement(parsed);
155
+ }
156
+
157
+ /**
158
+ * DinouCacheSlot component.
159
+ *
160
+ * @param {object} props
161
+ * @param {string} [props.id] Unique slot identifier
162
+ * @param {string} [props.tag] Cache tag for granular revalidateTag invalidation
163
+ * @param {string[]} [props.tags] Array of cache tags
164
+ * @param {number} [props.revalidate] Revalidation TTL in seconds (0 = no-cache, >0 = ISR)
165
+ * @param {React.ReactNode | Function} props.children Child component(s)
166
+ */
167
+ async function DinouCacheSlot(props) {
168
+ const { id, tag, tags, revalidate, children } = props || {};
169
+
170
+ const slotId = id || tag || (Array.isArray(tags) ? tags[0] : null);
171
+ if (!slotId) {
172
+ return typeof children === "function" ? await children() : children;
173
+ }
174
+
175
+ const allTags = Array.isArray(tags)
176
+ ? [...tags]
177
+ : tag
178
+ ? [tag]
179
+ : [];
180
+
181
+ const storageKey = `slots/${slotId}/slot.json`;
182
+ const storage = getStorageAdapter();
183
+
184
+ function wrapWithBoundary(content) {
185
+ const Boundary = getCacheSlotBoundary();
186
+ if (!Boundary) return content;
187
+ return React.createElement(Boundary, { id: slotId }, content);
188
+ }
189
+
190
+ // Register in slotRegistry for on-demand endpoint re-rendering
191
+ slotRegistry.set(slotId, {
192
+ render: async () => {
193
+ const childElement = typeof children === "function" ? await children() : children;
194
+ return await asyncRenderJSXToClientJSX(childElement);
195
+ },
196
+ tags: allTags,
197
+ revalidate: typeof revalidate === "number" ? revalidate : undefined,
198
+ });
199
+
200
+ if (!storage) {
201
+ const directContent = typeof children === "function" ? await children() : children;
202
+ return wrapWithBoundary(directContent);
203
+ }
204
+
205
+ // 1. Check if cached in storage
206
+ let cached = null;
207
+ try {
208
+ cached = await storage.get(storageKey);
209
+ } catch (err) {}
210
+
211
+ const now = Date.now();
212
+ if (cached && cached.content) {
213
+ const meta = cached.metadata || {};
214
+ const generatedAt = meta.generatedAt || 0;
215
+ const revalidateSeconds =
216
+ typeof revalidate === "number"
217
+ ? revalidate
218
+ : typeof meta.revalidate === "number"
219
+ ? meta.revalidate
220
+ : undefined;
221
+
222
+ const isExpired =
223
+ typeof revalidateSeconds === "number" &&
224
+ revalidateSeconds > 0 &&
225
+ now > generatedAt + revalidateSeconds * 1000;
226
+
227
+ if (!isExpired) {
228
+ try {
229
+ const restored = deserializeJSX(cached.content);
230
+ return wrapWithBoundary(restored);
231
+ } catch (err) {}
232
+ } else {
233
+ // Stale-While-Revalidate
234
+ try {
235
+ const staleJSX = deserializeJSX(cached.content);
236
+
237
+ if (!backgroundRevalidations.has(storageKey)) {
238
+ backgroundRevalidations.add(storageKey);
239
+ const bgPromise = (async () => {
240
+ try {
241
+ const childElement = typeof children === "function" ? await children() : children;
242
+ const resolved = await asyncRenderJSXToClientJSX(childElement);
243
+ const serialized = serializeJSX(resolved);
244
+ await storage.set(storageKey, serialized, {
245
+ tags: allTags,
246
+ revalidate: revalidateSeconds,
247
+ generatedAt: Date.now(),
248
+ slotId,
249
+ });
250
+ } catch (e) {
251
+ console.error(`[DinouCacheSlot] Background revalidation failed for slot "${slotId}":`, e);
252
+ } finally {
253
+ backgroundRevalidations.delete(storageKey);
254
+ }
255
+ })();
256
+
257
+ try {
258
+ const { getContext } = require("./request-context.js");
259
+ const reqContext = getContext();
260
+ const ctx = reqContext?.ctx || reqContext?.platformContext?.ctx || reqContext?.req?.ctx;
261
+ if (ctx && typeof ctx.waitUntil === "function") {
262
+ ctx.waitUntil(bgPromise);
263
+ }
264
+ } catch (e) {}
265
+ }
266
+
267
+ return wrapWithBoundary(staleJSX);
268
+ } catch (err) {}
269
+ }
270
+ }
271
+
272
+ // 2. Cache MISS or expired without usable stale content: Deduplicate synchronous render
273
+ if (inflightSlotRenders.has(storageKey)) {
274
+ try {
275
+ const deduped = await inflightSlotRenders.get(storageKey);
276
+ return wrapWithBoundary(deduped);
277
+ } catch (e) {}
278
+ }
279
+
280
+ const renderPromise = (async () => {
281
+ const childElement = typeof children === "function" ? await children() : children;
282
+ const resolved = await asyncRenderJSXToClientJSX(childElement);
283
+ const serialized = serializeJSX(resolved);
284
+
285
+ try {
286
+ await storage.set(storageKey, serialized, {
287
+ tags: allTags,
288
+ revalidate: typeof revalidate === "number" ? revalidate : undefined,
289
+ generatedAt: Date.now(),
290
+ slotId,
291
+ });
292
+ } catch (err) {
293
+ console.error(`[DinouCacheSlot] Failed to write cache for slot "${slotId}":`, err);
294
+ }
295
+
296
+ return deserializeJSX(serialized);
297
+ })();
298
+
299
+ inflightSlotRenders.set(storageKey, renderPromise);
300
+ try {
301
+ const fresh = await renderPromise;
302
+ return wrapWithBoundary(fresh);
303
+ } finally {
304
+ inflightSlotRenders.delete(storageKey);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Resolves a slot's JSX tree for the granular /____rsc_slot____/:id endpoint.
310
+ */
311
+ async function getSlotJSX(slotId, options = {}) {
312
+ const storage = getStorageAdapter();
313
+ const storageKey = `slots/${slotId}/slot.json`;
314
+
315
+ // 1. Check storage cache
316
+ let cached = null;
317
+ if (storage) {
318
+ try {
319
+ cached = await storage.get(storageKey);
320
+ } catch (e) {}
321
+ }
322
+
323
+ const now = Date.now();
324
+ if (cached && cached.content) {
325
+ const meta = cached.metadata || {};
326
+ const generatedAt = meta.generatedAt || 0;
327
+ const revalidateSeconds =
328
+ typeof meta.revalidate === "number" ? meta.revalidate : undefined;
329
+
330
+ const isExpired =
331
+ typeof revalidateSeconds === "number" &&
332
+ revalidateSeconds > 0 &&
333
+ now > generatedAt + revalidateSeconds * 1000;
334
+
335
+ if (!isExpired && !options.fresh) {
336
+ try {
337
+ return deserializeJSX(cached.content);
338
+ } catch (e) {}
339
+ }
340
+ }
341
+
342
+ // 2. If missing from storage (e.g. was invalidated) or expired or forced fresh:
343
+ if (!slotRegistry.has(slotId) && options.currentPath) {
344
+ try {
345
+ const { getJSX } = require("./get-jsx.js");
346
+ const isNotFound = { value: false };
347
+ await getJSX(options.currentPath, {}, isNotFound, false, false, { segment: "page" });
348
+ } catch (e) {
349
+ console.warn(`[getSlotJSX] Could not pre-evaluate route "${options.currentPath}" to register slot "${slotId}":`, e);
350
+ }
351
+ }
352
+
353
+ if (slotRegistry.has(slotId)) {
354
+ const entry = slotRegistry.get(slotId);
355
+ try {
356
+ const resolved = await entry.render();
357
+ const serialized = serializeJSX(resolved);
358
+ if (storage) {
359
+ await storage.set(storageKey, serialized, {
360
+ tags: entry.tags,
361
+ revalidate: entry.revalidate,
362
+ generatedAt: Date.now(),
363
+ slotId,
364
+ });
365
+ }
366
+ return deserializeJSX(serialized);
367
+ } catch (e) {
368
+ console.error(`[getSlotJSX] Failed to regenerate slot "${slotId}":`, e);
369
+ }
370
+ }
371
+
372
+ // Fallback to cached content if regeneration failed
373
+ if (cached && cached.content) {
374
+ try {
375
+ return deserializeJSX(cached.content);
376
+ } catch (e) {}
377
+ }
378
+
379
+ return null;
380
+ }
381
+
382
+ module.exports = {
383
+ DinouCacheSlot,
384
+ getSlotJSX,
385
+ serializeJSX,
386
+ deserializeJSX,
387
+ };
@@ -1,22 +1,27 @@
1
1
  // dinou/core/client-redirect.jsx
2
2
  "use client";
3
3
 
4
+ import { useEffect } from "react";
4
5
  import { useRouter } from "./navigation.js";
5
6
 
6
7
  export function ClientRedirect({ to }) {
7
8
  const router = useRouter();
8
9
 
9
- if (typeof window !== "undefined") {
10
- Promise.resolve().then(() => {
10
+ useEffect(() => {
11
+ if (typeof window !== "undefined") {
12
+ document.body.setAttribute("data-hydrated", "true");
11
13
  if (window.__DINOU_ROUTER_NAVIGATE__) {
12
14
  window.__DINOU_ROUTER_NAVIGATE__(to, { replace: true });
13
- } else {
15
+ } else if (router && router.replace) {
14
16
  router.replace(to);
17
+ } else {
18
+ window.location.href = to;
15
19
  }
16
- });
17
- // Suspend to prevent React from committing this intermediate page
18
- throw new Promise(() => {});
19
- }
20
+ }
21
+ }, [to, router]);
20
22
 
21
23
  return null;
22
24
  }
25
+
26
+ export default ClientRedirect;
27
+