@lolyjs/core 0.2.0-alpha.2 → 0.2.0-alpha.20
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/LICENCE.md +9 -0
- package/README.md +1074 -761
- package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
- package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
- package/dist/cli.cjs +16933 -4416
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +16943 -4416
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +14387 -1372
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +295 -57
- package/dist/index.d.ts +295 -57
- package/dist/index.js +15621 -2597
- package/dist/index.js.map +1 -1
- package/dist/index.types-DMOO-uvF.d.mts +221 -0
- package/dist/index.types-DMOO-uvF.d.ts +221 -0
- package/dist/react/cache.cjs +107 -32
- package/dist/react/cache.cjs.map +1 -1
- package/dist/react/cache.d.mts +27 -21
- package/dist/react/cache.d.ts +27 -21
- package/dist/react/cache.js +107 -32
- package/dist/react/cache.js.map +1 -1
- package/dist/react/components.cjs +10 -8
- package/dist/react/components.cjs.map +1 -1
- package/dist/react/components.js +10 -8
- package/dist/react/components.js.map +1 -1
- package/dist/react/hooks.cjs +208 -26
- package/dist/react/hooks.cjs.map +1 -1
- package/dist/react/hooks.d.mts +75 -15
- package/dist/react/hooks.d.ts +75 -15
- package/dist/react/hooks.js +208 -26
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/sockets.cjs +13 -6
- package/dist/react/sockets.cjs.map +1 -1
- package/dist/react/sockets.js +13 -6
- package/dist/react/sockets.js.map +1 -1
- package/dist/react/themes.cjs +61 -18
- package/dist/react/themes.cjs.map +1 -1
- package/dist/react/themes.js +63 -20
- package/dist/react/themes.js.map +1 -1
- package/dist/runtime.cjs +544 -111
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +540 -107
- package/dist/runtime.js.map +1 -1
- package/package.json +49 -4
package/dist/runtime.js
CHANGED
|
@@ -3,12 +3,40 @@ import { hydrateRoot } from "react-dom/client";
|
|
|
3
3
|
|
|
4
4
|
// modules/runtime/client/constants.ts
|
|
5
5
|
var WINDOW_DATA_KEY = "__FW_DATA__";
|
|
6
|
+
var ROUTER_DATA_KEY = "__LOLY_ROUTER_DATA__";
|
|
6
7
|
var APP_CONTAINER_ID = "__app";
|
|
8
|
+
var ROUTER_NAVIGATE_KEY = "__LOLY_ROUTER_NAVIGATE__";
|
|
7
9
|
|
|
8
10
|
// modules/runtime/client/window-data.ts
|
|
11
|
+
var LAYOUT_PROPS_KEY = "__FW_LAYOUT_PROPS__";
|
|
9
12
|
function getWindowData() {
|
|
13
|
+
if (typeof window === "undefined") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
10
16
|
return window[WINDOW_DATA_KEY] ?? null;
|
|
11
17
|
}
|
|
18
|
+
function getPreservedLayoutProps() {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return window[LAYOUT_PROPS_KEY] ?? null;
|
|
23
|
+
}
|
|
24
|
+
function setPreservedLayoutProps(props) {
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (props === null) {
|
|
29
|
+
delete window[LAYOUT_PROPS_KEY];
|
|
30
|
+
} else {
|
|
31
|
+
window[LAYOUT_PROPS_KEY] = props;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getRouterData() {
|
|
35
|
+
if (typeof window === "undefined") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return window[ROUTER_DATA_KEY] ?? null;
|
|
39
|
+
}
|
|
12
40
|
function setWindowData(data) {
|
|
13
41
|
window[WINDOW_DATA_KEY] = data;
|
|
14
42
|
if (typeof window !== "undefined") {
|
|
@@ -19,6 +47,16 @@ function setWindowData(data) {
|
|
|
19
47
|
);
|
|
20
48
|
}
|
|
21
49
|
}
|
|
50
|
+
function setRouterData(data) {
|
|
51
|
+
window[ROUTER_DATA_KEY] = data;
|
|
52
|
+
if (typeof window !== "undefined") {
|
|
53
|
+
window.dispatchEvent(
|
|
54
|
+
new CustomEvent("fw-router-data-refresh", {
|
|
55
|
+
detail: { data }
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
22
60
|
function getCurrentTheme() {
|
|
23
61
|
return getWindowData()?.theme ?? null;
|
|
24
62
|
}
|
|
@@ -64,26 +102,162 @@ function matchRouteClient(pathWithSearch, routes) {
|
|
|
64
102
|
}
|
|
65
103
|
|
|
66
104
|
// modules/runtime/client/metadata.ts
|
|
105
|
+
function getOrCreateMeta(selector, attributes) {
|
|
106
|
+
let meta = document.querySelector(selector);
|
|
107
|
+
if (!meta) {
|
|
108
|
+
meta = document.createElement("meta");
|
|
109
|
+
if (attributes.name) meta.name = attributes.name;
|
|
110
|
+
if (attributes.property) meta.setAttribute("property", attributes.property);
|
|
111
|
+
if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
|
|
112
|
+
document.head.appendChild(meta);
|
|
113
|
+
}
|
|
114
|
+
return meta;
|
|
115
|
+
}
|
|
116
|
+
function getOrCreateLink(rel, href) {
|
|
117
|
+
const selector = `link[rel="${rel}"]`;
|
|
118
|
+
let link = document.querySelector(selector);
|
|
119
|
+
if (!link) {
|
|
120
|
+
link = document.createElement("link");
|
|
121
|
+
link.rel = rel;
|
|
122
|
+
link.href = href;
|
|
123
|
+
document.head.appendChild(link);
|
|
124
|
+
} else {
|
|
125
|
+
link.href = href;
|
|
126
|
+
}
|
|
127
|
+
return link;
|
|
128
|
+
}
|
|
67
129
|
function applyMetadata(md) {
|
|
68
130
|
if (!md) return;
|
|
69
131
|
if (md.title) {
|
|
70
132
|
document.title = md.title;
|
|
71
133
|
}
|
|
72
134
|
if (md.description) {
|
|
73
|
-
|
|
74
|
-
'meta[name="description"]'
|
|
75
|
-
);
|
|
76
|
-
if (!meta) {
|
|
77
|
-
meta = document.createElement("meta");
|
|
78
|
-
meta.name = "description";
|
|
79
|
-
document.head.appendChild(meta);
|
|
80
|
-
}
|
|
135
|
+
const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
|
|
81
136
|
meta.content = md.description;
|
|
82
137
|
}
|
|
138
|
+
if (md.robots) {
|
|
139
|
+
const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
|
|
140
|
+
meta.content = md.robots;
|
|
141
|
+
}
|
|
142
|
+
if (md.themeColor) {
|
|
143
|
+
const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
|
|
144
|
+
meta.content = md.themeColor;
|
|
145
|
+
}
|
|
146
|
+
if (md.viewport) {
|
|
147
|
+
const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
|
|
148
|
+
meta.content = md.viewport;
|
|
149
|
+
}
|
|
150
|
+
if (md.canonical) {
|
|
151
|
+
getOrCreateLink("canonical", md.canonical);
|
|
152
|
+
}
|
|
153
|
+
if (md.openGraph) {
|
|
154
|
+
const og = md.openGraph;
|
|
155
|
+
if (og.title) {
|
|
156
|
+
const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
|
|
157
|
+
meta.content = og.title;
|
|
158
|
+
}
|
|
159
|
+
if (og.description) {
|
|
160
|
+
const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
|
|
161
|
+
meta.content = og.description;
|
|
162
|
+
}
|
|
163
|
+
if (og.type) {
|
|
164
|
+
const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
|
|
165
|
+
meta.content = og.type;
|
|
166
|
+
}
|
|
167
|
+
if (og.url) {
|
|
168
|
+
const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
|
|
169
|
+
meta.content = og.url;
|
|
170
|
+
}
|
|
171
|
+
if (og.image) {
|
|
172
|
+
if (typeof og.image === "string") {
|
|
173
|
+
const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
|
|
174
|
+
meta.content = og.image;
|
|
175
|
+
} else {
|
|
176
|
+
const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
|
|
177
|
+
meta.content = og.image.url;
|
|
178
|
+
if (og.image.width) {
|
|
179
|
+
const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
|
|
180
|
+
metaWidth.content = String(og.image.width);
|
|
181
|
+
}
|
|
182
|
+
if (og.image.height) {
|
|
183
|
+
const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
|
|
184
|
+
metaHeight.content = String(og.image.height);
|
|
185
|
+
}
|
|
186
|
+
if (og.image.alt) {
|
|
187
|
+
const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
|
|
188
|
+
metaAlt.content = og.image.alt;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (og.siteName) {
|
|
193
|
+
const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
|
|
194
|
+
meta.content = og.siteName;
|
|
195
|
+
}
|
|
196
|
+
if (og.locale) {
|
|
197
|
+
const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
|
|
198
|
+
meta.content = og.locale;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (md.twitter) {
|
|
202
|
+
const twitter = md.twitter;
|
|
203
|
+
if (twitter.card) {
|
|
204
|
+
const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
|
|
205
|
+
meta.content = twitter.card;
|
|
206
|
+
}
|
|
207
|
+
if (twitter.title) {
|
|
208
|
+
const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
|
|
209
|
+
meta.content = twitter.title;
|
|
210
|
+
}
|
|
211
|
+
if (twitter.description) {
|
|
212
|
+
const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
|
|
213
|
+
meta.content = twitter.description;
|
|
214
|
+
}
|
|
215
|
+
if (twitter.image) {
|
|
216
|
+
const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
|
|
217
|
+
meta.content = twitter.image;
|
|
218
|
+
}
|
|
219
|
+
if (twitter.imageAlt) {
|
|
220
|
+
const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
|
|
221
|
+
meta.content = twitter.imageAlt;
|
|
222
|
+
}
|
|
223
|
+
if (twitter.site) {
|
|
224
|
+
const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
|
|
225
|
+
meta.content = twitter.site;
|
|
226
|
+
}
|
|
227
|
+
if (twitter.creator) {
|
|
228
|
+
const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
|
|
229
|
+
meta.content = twitter.creator;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (md.metaTags && Array.isArray(md.metaTags)) {
|
|
233
|
+
md.metaTags.forEach((tag) => {
|
|
234
|
+
let selector = "";
|
|
235
|
+
if (tag.name) {
|
|
236
|
+
selector = `meta[name="${tag.name}"]`;
|
|
237
|
+
} else if (tag.property) {
|
|
238
|
+
selector = `meta[property="${tag.property}"]`;
|
|
239
|
+
} else if (tag.httpEquiv) {
|
|
240
|
+
selector = `meta[http-equiv="${tag.httpEquiv}"]`;
|
|
241
|
+
}
|
|
242
|
+
if (selector) {
|
|
243
|
+
const meta = getOrCreateMeta(selector, {
|
|
244
|
+
name: tag.name,
|
|
245
|
+
property: tag.property,
|
|
246
|
+
httpEquiv: tag.httpEquiv
|
|
247
|
+
});
|
|
248
|
+
meta.content = tag.content;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (md.links && Array.isArray(md.links)) {
|
|
253
|
+
md.links.forEach((link) => {
|
|
254
|
+
getOrCreateLink(link.rel, link.href);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
83
257
|
}
|
|
84
258
|
|
|
85
259
|
// modules/runtime/client/AppShell.tsx
|
|
86
|
-
import { useEffect, useState, useRef } from "react";
|
|
260
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
87
261
|
|
|
88
262
|
// modules/runtime/client/RouterView.tsx
|
|
89
263
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -192,14 +366,16 @@ function deleteCacheEntry(key) {
|
|
|
192
366
|
function buildDataUrl(url) {
|
|
193
367
|
return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
|
|
194
368
|
}
|
|
195
|
-
async function fetchRouteDataOnce(url) {
|
|
369
|
+
async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
|
|
196
370
|
const dataUrl = buildDataUrl(url);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
371
|
+
const headers = {
|
|
372
|
+
"x-fw-data": "1",
|
|
373
|
+
Accept: "application/json"
|
|
374
|
+
};
|
|
375
|
+
if (skipLayoutHooks) {
|
|
376
|
+
headers["x-skip-layout-hooks"] = "true";
|
|
377
|
+
}
|
|
378
|
+
const res = await fetch(dataUrl, { headers });
|
|
203
379
|
let json = {};
|
|
204
380
|
try {
|
|
205
381
|
const text = await res.text();
|
|
@@ -225,7 +401,7 @@ async function getRouteData(url, options) {
|
|
|
225
401
|
deleteCacheEntry(key);
|
|
226
402
|
}
|
|
227
403
|
const entry = dataCache.get(key);
|
|
228
|
-
if (entry) {
|
|
404
|
+
if (entry && !options?.revalidate) {
|
|
229
405
|
if (entry.status === "fulfilled") {
|
|
230
406
|
updateLRU(key);
|
|
231
407
|
return entry.value;
|
|
@@ -234,12 +410,29 @@ async function getRouteData(url, options) {
|
|
|
234
410
|
return entry.promise;
|
|
235
411
|
}
|
|
236
412
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
413
|
+
const skipLayoutHooks = !options?.revalidate;
|
|
414
|
+
const currentEntry = dataCache.get(key);
|
|
415
|
+
if (currentEntry && !options?.revalidate) {
|
|
416
|
+
if (currentEntry.status === "fulfilled") {
|
|
417
|
+
updateLRU(key);
|
|
418
|
+
return currentEntry.value;
|
|
419
|
+
}
|
|
420
|
+
if (currentEntry.status === "pending") {
|
|
421
|
+
return currentEntry.promise;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const promise = fetchRouteDataOnce(url, skipLayoutHooks).then((value) => {
|
|
425
|
+
const entryAfterFetch = dataCache.get(key);
|
|
426
|
+
if (!entryAfterFetch || entryAfterFetch.status === "pending") {
|
|
427
|
+
setCacheEntry(key, { status: "fulfilled", value });
|
|
428
|
+
}
|
|
239
429
|
return value;
|
|
240
430
|
}).catch((error) => {
|
|
241
431
|
console.error("[client][cache] Error fetching route data:", error);
|
|
242
|
-
dataCache.
|
|
432
|
+
const entryAfterFetch = dataCache.get(key);
|
|
433
|
+
if (!entryAfterFetch || entryAfterFetch.status === "pending") {
|
|
434
|
+
dataCache.set(key, { status: "rejected", error });
|
|
435
|
+
}
|
|
243
436
|
throw error;
|
|
244
437
|
});
|
|
245
438
|
dataCache.set(key, { status: "pending", promise });
|
|
@@ -264,10 +457,22 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
|
|
|
264
457
|
} else if (json.theme) {
|
|
265
458
|
theme = json.theme;
|
|
266
459
|
}
|
|
460
|
+
let layoutProps = {};
|
|
461
|
+
if (json.layoutProps !== void 0 && json.layoutProps !== null) {
|
|
462
|
+
layoutProps = json.layoutProps;
|
|
463
|
+
setPreservedLayoutProps(layoutProps);
|
|
464
|
+
} else {
|
|
465
|
+
const preserved = getPreservedLayoutProps();
|
|
466
|
+
if (preserved) {
|
|
467
|
+
layoutProps = preserved;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const pageProps = json.pageProps ?? json.props ?? {
|
|
471
|
+
error: json.message || "An error occurred"
|
|
472
|
+
};
|
|
267
473
|
const errorProps = {
|
|
268
|
-
...
|
|
269
|
-
|
|
270
|
-
},
|
|
474
|
+
...layoutProps,
|
|
475
|
+
...pageProps,
|
|
271
476
|
theme
|
|
272
477
|
};
|
|
273
478
|
const windowData = {
|
|
@@ -280,6 +485,13 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
|
|
|
280
485
|
error: true
|
|
281
486
|
};
|
|
282
487
|
setWindowData(windowData);
|
|
488
|
+
const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
489
|
+
const routerData = {
|
|
490
|
+
pathname: url.pathname,
|
|
491
|
+
params: json.params || {},
|
|
492
|
+
searchParams: Object.fromEntries(url.searchParams.entries())
|
|
493
|
+
};
|
|
494
|
+
setRouterData(routerData);
|
|
283
495
|
setState({
|
|
284
496
|
url: nextUrl,
|
|
285
497
|
route: errorRoute,
|
|
@@ -289,10 +501,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
|
|
|
289
501
|
});
|
|
290
502
|
return true;
|
|
291
503
|
} catch (loadError) {
|
|
292
|
-
console.error(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
504
|
+
console.error("\n\u274C [client] Error loading error route components:");
|
|
505
|
+
console.error(loadError);
|
|
506
|
+
if (loadError instanceof Error) {
|
|
507
|
+
console.error(` Message: ${loadError.message}`);
|
|
508
|
+
if (loadError.stack) {
|
|
509
|
+
console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
console.error("\u{1F4A1} Falling back to full page reload\n");
|
|
296
513
|
window.location.href = nextUrl;
|
|
297
514
|
return false;
|
|
298
515
|
}
|
|
@@ -312,8 +529,20 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
|
|
|
312
529
|
} else if (json.theme) {
|
|
313
530
|
theme = json.theme;
|
|
314
531
|
}
|
|
532
|
+
let layoutProps = {};
|
|
533
|
+
if (json.layoutProps !== void 0 && json.layoutProps !== null) {
|
|
534
|
+
layoutProps = json.layoutProps;
|
|
535
|
+
setPreservedLayoutProps(layoutProps);
|
|
536
|
+
} else {
|
|
537
|
+
const preserved = getPreservedLayoutProps();
|
|
538
|
+
if (preserved) {
|
|
539
|
+
layoutProps = preserved;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const pageProps = json.pageProps ?? json.props ?? {};
|
|
315
543
|
const notFoundProps = {
|
|
316
|
-
...
|
|
544
|
+
...layoutProps,
|
|
545
|
+
...pageProps,
|
|
317
546
|
theme
|
|
318
547
|
};
|
|
319
548
|
const windowData = {
|
|
@@ -326,6 +555,13 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
|
|
|
326
555
|
error: false
|
|
327
556
|
};
|
|
328
557
|
setWindowData(windowData);
|
|
558
|
+
const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
559
|
+
const routerData = {
|
|
560
|
+
pathname: url.pathname,
|
|
561
|
+
params: {},
|
|
562
|
+
searchParams: Object.fromEntries(url.searchParams.entries())
|
|
563
|
+
};
|
|
564
|
+
setRouterData(routerData);
|
|
329
565
|
if (notFoundRoute) {
|
|
330
566
|
const components = await notFoundRoute.load();
|
|
331
567
|
setState({
|
|
@@ -363,8 +599,20 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
|
|
|
363
599
|
} else if (json.theme) {
|
|
364
600
|
theme = json.theme;
|
|
365
601
|
}
|
|
366
|
-
|
|
367
|
-
|
|
602
|
+
let layoutProps = {};
|
|
603
|
+
if (json.layoutProps !== void 0 && json.layoutProps !== null) {
|
|
604
|
+
layoutProps = json.layoutProps;
|
|
605
|
+
setPreservedLayoutProps(layoutProps);
|
|
606
|
+
} else {
|
|
607
|
+
const preserved = getPreservedLayoutProps();
|
|
608
|
+
if (preserved) {
|
|
609
|
+
layoutProps = preserved;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const pageProps = json.pageProps ?? json.props ?? {};
|
|
613
|
+
const combinedProps = {
|
|
614
|
+
...layoutProps,
|
|
615
|
+
...pageProps,
|
|
368
616
|
theme
|
|
369
617
|
// Always include theme
|
|
370
618
|
};
|
|
@@ -376,14 +624,25 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
|
|
|
376
624
|
const windowData = {
|
|
377
625
|
pathname: nextUrl,
|
|
378
626
|
params: matched.params,
|
|
379
|
-
props:
|
|
627
|
+
props: combinedProps,
|
|
380
628
|
metadata: json.metadata ?? null,
|
|
381
629
|
theme,
|
|
382
630
|
notFound: false,
|
|
383
631
|
error: false
|
|
384
632
|
};
|
|
385
633
|
setWindowData(windowData);
|
|
386
|
-
const
|
|
634
|
+
const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
635
|
+
const routerData = {
|
|
636
|
+
pathname: url.pathname,
|
|
637
|
+
params: matched.params,
|
|
638
|
+
searchParams: Object.fromEntries(url.searchParams.entries())
|
|
639
|
+
};
|
|
640
|
+
setRouterData(routerData);
|
|
641
|
+
const prefetched = prefetchedRoutes.get(matched.route);
|
|
642
|
+
const components = prefetched ? await prefetched : await matched.route.load();
|
|
643
|
+
if (!prefetched) {
|
|
644
|
+
prefetchedRoutes.set(matched.route, Promise.resolve(components));
|
|
645
|
+
}
|
|
387
646
|
window.scrollTo({
|
|
388
647
|
top: 0,
|
|
389
648
|
behavior: "smooth"
|
|
@@ -393,7 +652,7 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
|
|
|
393
652
|
route: matched.route,
|
|
394
653
|
params: matched.params,
|
|
395
654
|
components,
|
|
396
|
-
props:
|
|
655
|
+
props: combinedProps
|
|
397
656
|
});
|
|
398
657
|
return true;
|
|
399
658
|
}
|
|
@@ -422,7 +681,7 @@ async function navigate(nextUrl, handlers, options) {
|
|
|
422
681
|
}
|
|
423
682
|
}
|
|
424
683
|
if (!ok) {
|
|
425
|
-
if (json
|
|
684
|
+
if (json?.redirect) {
|
|
426
685
|
window.location.href = json.redirect.destination;
|
|
427
686
|
return;
|
|
428
687
|
}
|
|
@@ -443,6 +702,47 @@ async function navigate(nextUrl, handlers, options) {
|
|
|
443
702
|
window.location.href = nextUrl;
|
|
444
703
|
}
|
|
445
704
|
}
|
|
705
|
+
var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
|
|
706
|
+
function prefetchRoute(url, routes, notFoundRoute) {
|
|
707
|
+
const [pathname] = url.split("?");
|
|
708
|
+
const matched = matchRouteClient(pathname, routes);
|
|
709
|
+
if (!matched) {
|
|
710
|
+
if (notFoundRoute) {
|
|
711
|
+
const existing2 = prefetchedRoutes.get(notFoundRoute);
|
|
712
|
+
if (!existing2) {
|
|
713
|
+
const promise = notFoundRoute.load();
|
|
714
|
+
prefetchedRoutes.set(notFoundRoute, promise);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const existing = prefetchedRoutes.get(matched.route);
|
|
720
|
+
if (!existing) {
|
|
721
|
+
const promise = matched.route.load();
|
|
722
|
+
prefetchedRoutes.set(matched.route, promise);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function createHoverHandler(routes, notFoundRoute) {
|
|
726
|
+
return function handleHover(ev) {
|
|
727
|
+
try {
|
|
728
|
+
const target = ev.target;
|
|
729
|
+
if (!target) return;
|
|
730
|
+
const anchor = target.closest("a[href]");
|
|
731
|
+
if (!anchor) return;
|
|
732
|
+
const href = anchor.getAttribute("href");
|
|
733
|
+
if (!href) return;
|
|
734
|
+
if (href.startsWith("#")) return;
|
|
735
|
+
const url = new URL(href, window.location.href);
|
|
736
|
+
if (url.origin !== window.location.origin) return;
|
|
737
|
+
if (anchor.target && anchor.target !== "_self") return;
|
|
738
|
+
const nextUrl = url.pathname + url.search;
|
|
739
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
740
|
+
if (nextUrl === currentUrl) return;
|
|
741
|
+
prefetchRoute(nextUrl, routes, notFoundRoute);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
446
746
|
function createClickHandler(navigate2) {
|
|
447
747
|
return function handleClick(ev) {
|
|
448
748
|
try {
|
|
@@ -502,6 +802,10 @@ function createPopStateHandler(navigate2) {
|
|
|
502
802
|
};
|
|
503
803
|
}
|
|
504
804
|
|
|
805
|
+
// modules/runtime/client/RouterContext.tsx
|
|
806
|
+
import { createContext, useContext } from "react";
|
|
807
|
+
var RouterContext = createContext(null);
|
|
808
|
+
|
|
505
809
|
// modules/runtime/client/AppShell.tsx
|
|
506
810
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
507
811
|
function AppShell({
|
|
@@ -525,27 +829,153 @@ function AppShell({
|
|
|
525
829
|
errorRoute
|
|
526
830
|
};
|
|
527
831
|
}, [routes, notFoundRoute, errorRoute]);
|
|
832
|
+
const handleNavigate = useCallback(
|
|
833
|
+
async (nextUrl, options) => {
|
|
834
|
+
await navigate(nextUrl, handlersRef.current, {
|
|
835
|
+
revalidate: options?.revalidate
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
[]
|
|
839
|
+
);
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
if (typeof window !== "undefined") {
|
|
842
|
+
window[ROUTER_NAVIGATE_KEY] = handleNavigate;
|
|
843
|
+
return () => {
|
|
844
|
+
delete window[ROUTER_NAVIGATE_KEY];
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}, [handleNavigate]);
|
|
528
848
|
useEffect(() => {
|
|
529
849
|
let isMounted = true;
|
|
530
|
-
async function
|
|
850
|
+
async function handleNavigateInternal(nextUrl, options) {
|
|
531
851
|
if (!isMounted) return;
|
|
532
852
|
await navigate(nextUrl, handlersRef.current, options);
|
|
533
853
|
}
|
|
534
|
-
const handleClick = createClickHandler(
|
|
535
|
-
const handlePopState = createPopStateHandler(
|
|
854
|
+
const handleClick = createClickHandler(handleNavigateInternal);
|
|
855
|
+
const handlePopState = createPopStateHandler(handleNavigateInternal);
|
|
856
|
+
const handleHover = createHoverHandler(routes, notFoundRoute);
|
|
536
857
|
window.addEventListener("click", handleClick, false);
|
|
537
858
|
window.addEventListener("popstate", handlePopState, false);
|
|
859
|
+
window.addEventListener("mouseover", handleHover, false);
|
|
538
860
|
return () => {
|
|
539
861
|
isMounted = false;
|
|
540
862
|
window.removeEventListener("click", handleClick, false);
|
|
541
863
|
window.removeEventListener("popstate", handlePopState, false);
|
|
864
|
+
window.removeEventListener("mouseover", handleHover, false);
|
|
865
|
+
};
|
|
866
|
+
}, [routes, notFoundRoute]);
|
|
867
|
+
useEffect(() => {
|
|
868
|
+
const handleDataRefresh = () => {
|
|
869
|
+
const freshData = window[WINDOW_DATA_KEY];
|
|
870
|
+
if (!freshData) return;
|
|
871
|
+
const currentPathname = window.location.pathname;
|
|
872
|
+
const freshPathname = freshData.pathname;
|
|
873
|
+
if (freshPathname === currentPathname) {
|
|
874
|
+
if (freshData.metadata !== void 0) {
|
|
875
|
+
applyMetadata(freshData.metadata);
|
|
876
|
+
}
|
|
877
|
+
setState((prevState) => ({
|
|
878
|
+
...prevState,
|
|
879
|
+
props: freshData.props ?? prevState.props,
|
|
880
|
+
params: freshData.params ?? prevState.params
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
window.addEventListener("fw-data-refresh", handleDataRefresh);
|
|
885
|
+
return () => {
|
|
886
|
+
window.removeEventListener("fw-data-refresh", handleDataRefresh);
|
|
542
887
|
};
|
|
543
888
|
}, []);
|
|
544
889
|
const isError = state.route === errorRoute;
|
|
545
890
|
const isNotFound = state.route === notFoundRoute;
|
|
546
891
|
const routeType = isError ? "error" : isNotFound ? "notfound" : "normal";
|
|
547
892
|
const routeKey = `${state.url}:${routeType}`;
|
|
548
|
-
return /* @__PURE__ */ jsx2(RouterView, { state }, routeKey);
|
|
893
|
+
return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// modules/runtime/client/hot-reload.ts
|
|
897
|
+
function setupHotReload() {
|
|
898
|
+
const nodeEnv = process.env.NODE_ENV || "production";
|
|
899
|
+
const isDev = nodeEnv === "development";
|
|
900
|
+
console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
|
|
901
|
+
if (!isDev) {
|
|
902
|
+
console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
console.log("[hot-reload] Setting up hot reload client...");
|
|
906
|
+
let eventSource = null;
|
|
907
|
+
let reloadTimeout = null;
|
|
908
|
+
let reconnectTimeout = null;
|
|
909
|
+
let reconnectAttempts = 0;
|
|
910
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
911
|
+
const RECONNECT_DELAY = 1e3;
|
|
912
|
+
const RELOAD_DELAY = 100;
|
|
913
|
+
function connect() {
|
|
914
|
+
try {
|
|
915
|
+
if (eventSource) {
|
|
916
|
+
console.log("[hot-reload] Closing existing EventSource connection");
|
|
917
|
+
eventSource.close();
|
|
918
|
+
}
|
|
919
|
+
const endpoint = "/__fw/hot";
|
|
920
|
+
eventSource = new EventSource(endpoint);
|
|
921
|
+
eventSource.addEventListener("ping", (event) => {
|
|
922
|
+
if ("data" in event) {
|
|
923
|
+
console.log("[hot-reload] \u2705 Connected to hot reload server");
|
|
924
|
+
}
|
|
925
|
+
reconnectAttempts = 0;
|
|
926
|
+
});
|
|
927
|
+
eventSource.addEventListener("message", (event) => {
|
|
928
|
+
const data = event.data;
|
|
929
|
+
if (data && typeof data === "string" && data.startsWith("reload:")) {
|
|
930
|
+
const filePath = data.slice(7);
|
|
931
|
+
console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
|
|
932
|
+
if (reloadTimeout) {
|
|
933
|
+
clearTimeout(reloadTimeout);
|
|
934
|
+
}
|
|
935
|
+
reloadTimeout = setTimeout(() => {
|
|
936
|
+
try {
|
|
937
|
+
window.location.reload();
|
|
938
|
+
} catch (error) {
|
|
939
|
+
console.error("[hot-reload] \u274C Error reloading page:", error);
|
|
940
|
+
setTimeout(() => window.location.reload(), 100);
|
|
941
|
+
}
|
|
942
|
+
}, RELOAD_DELAY);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
eventSource.onopen = () => {
|
|
946
|
+
reconnectAttempts = 0;
|
|
947
|
+
};
|
|
948
|
+
eventSource.onerror = (error) => {
|
|
949
|
+
const states = ["CONNECTING", "OPEN", "CLOSED"];
|
|
950
|
+
const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
|
|
951
|
+
if (eventSource?.readyState === EventSource.CONNECTING) {
|
|
952
|
+
console.log("[hot-reload] \u23F3 Still connecting...");
|
|
953
|
+
return;
|
|
954
|
+
} else if (eventSource?.readyState === EventSource.OPEN) {
|
|
955
|
+
console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
|
|
956
|
+
} else {
|
|
957
|
+
console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
|
|
958
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
959
|
+
reconnectAttempts++;
|
|
960
|
+
const delay = RECONNECT_DELAY * reconnectAttempts;
|
|
961
|
+
if (reconnectTimeout) {
|
|
962
|
+
clearTimeout(reconnectTimeout);
|
|
963
|
+
}
|
|
964
|
+
reconnectTimeout = setTimeout(() => {
|
|
965
|
+
console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
966
|
+
connect();
|
|
967
|
+
}, delay);
|
|
968
|
+
} else {
|
|
969
|
+
console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
} catch (error) {
|
|
974
|
+
console.error("[hot-reload] \u274C Failed to create EventSource:", error);
|
|
975
|
+
console.error("[hot-reload] EventSource may not be supported in this browser.");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
connect();
|
|
549
979
|
}
|
|
550
980
|
|
|
551
981
|
// modules/runtime/client/bootstrap.tsx
|
|
@@ -589,91 +1019,94 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
|
|
|
589
1019
|
props: initialData?.props ?? {}
|
|
590
1020
|
};
|
|
591
1021
|
}
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1022
|
+
function initializeRouterData(initialUrl, initialData) {
|
|
1023
|
+
let routerData = getRouterData();
|
|
1024
|
+
if (!routerData) {
|
|
1025
|
+
const url = new URL(initialUrl, window.location.origin);
|
|
1026
|
+
routerData = {
|
|
1027
|
+
pathname: url.pathname,
|
|
1028
|
+
params: initialData?.params || {},
|
|
1029
|
+
searchParams: Object.fromEntries(url.searchParams.entries())
|
|
1030
|
+
};
|
|
1031
|
+
setRouterData(routerData);
|
|
597
1032
|
}
|
|
1033
|
+
}
|
|
1034
|
+
async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
|
|
598
1035
|
try {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
console.log("[hot-reload] Reloading page...");
|
|
612
|
-
window.location.reload();
|
|
613
|
-
}, 500);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
eventSource.addEventListener("ping", () => {
|
|
617
|
-
console.log("[hot-reload] \u2713 Connected to hot reload server");
|
|
618
|
-
});
|
|
619
|
-
eventSource.onopen = () => {
|
|
620
|
-
console.log("[hot-reload] \u2713 SSE connection opened");
|
|
621
|
-
};
|
|
622
|
-
eventSource.onerror = (error) => {
|
|
623
|
-
const states = ["CONNECTING", "OPEN", "CLOSED"];
|
|
624
|
-
const state = states[eventSource.readyState] || "UNKNOWN";
|
|
625
|
-
if (eventSource.readyState === EventSource.CONNECTING) {
|
|
626
|
-
console.log("[hot-reload] Connecting...");
|
|
627
|
-
} else if (eventSource.readyState === EventSource.OPEN) {
|
|
628
|
-
console.warn("[hot-reload] Connection error (but connection is open):", error);
|
|
629
|
-
} else {
|
|
630
|
-
console.log("[hot-reload] Connection closed (readyState:", state, ")");
|
|
1036
|
+
const initialState = await loadInitialRoute(
|
|
1037
|
+
initialUrl,
|
|
1038
|
+
initialData,
|
|
1039
|
+
routes,
|
|
1040
|
+
notFoundRoute,
|
|
1041
|
+
errorRoute
|
|
1042
|
+
);
|
|
1043
|
+
if (initialData?.metadata) {
|
|
1044
|
+
try {
|
|
1045
|
+
applyMetadata(initialData.metadata);
|
|
1046
|
+
} catch (metadataError) {
|
|
1047
|
+
console.warn("[client] Error applying metadata:", metadataError);
|
|
631
1048
|
}
|
|
632
|
-
}
|
|
1049
|
+
}
|
|
1050
|
+
hydrateRoot(
|
|
1051
|
+
container,
|
|
1052
|
+
/* @__PURE__ */ jsx3(
|
|
1053
|
+
AppShell,
|
|
1054
|
+
{
|
|
1055
|
+
initialState,
|
|
1056
|
+
routes,
|
|
1057
|
+
notFoundRoute,
|
|
1058
|
+
errorRoute
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
633
1062
|
} catch (error) {
|
|
634
|
-
console.
|
|
1063
|
+
console.error(
|
|
1064
|
+
"[client] Error loading initial route components for",
|
|
1065
|
+
initialUrl,
|
|
1066
|
+
error
|
|
1067
|
+
);
|
|
1068
|
+
throw error;
|
|
635
1069
|
}
|
|
636
1070
|
}
|
|
637
1071
|
function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
|
|
638
|
-
console.log("[client] Bootstrap starting, setting up hot reload...");
|
|
639
1072
|
setupHotReload();
|
|
640
|
-
(async
|
|
641
|
-
const container = document.getElementById(APP_CONTAINER_ID);
|
|
642
|
-
const initialData = getWindowData();
|
|
643
|
-
if (!container) {
|
|
644
|
-
console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
const initialUrl = window.location.pathname + window.location.search;
|
|
1073
|
+
(async () => {
|
|
648
1074
|
try {
|
|
649
|
-
const
|
|
1075
|
+
const container = document.getElementById(APP_CONTAINER_ID);
|
|
1076
|
+
if (!container) {
|
|
1077
|
+
console.error(`
|
|
1078
|
+
\u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
|
|
1079
|
+
console.error("\u{1F4A1} This usually means:");
|
|
1080
|
+
console.error(" \u2022 The HTML structure doesn't match what React expects");
|
|
1081
|
+
console.error(" \u2022 The container was removed before hydration");
|
|
1082
|
+
console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const initialData = getWindowData();
|
|
1086
|
+
const initialUrl = window.location.pathname + window.location.search;
|
|
1087
|
+
if (initialData?.props) {
|
|
1088
|
+
setPreservedLayoutProps(initialData.props);
|
|
1089
|
+
}
|
|
1090
|
+
initializeRouterData(initialUrl, initialData);
|
|
1091
|
+
await hydrateInitialRoute(
|
|
1092
|
+
container,
|
|
650
1093
|
initialUrl,
|
|
651
1094
|
initialData,
|
|
652
1095
|
routes,
|
|
653
1096
|
notFoundRoute,
|
|
654
1097
|
errorRoute
|
|
655
1098
|
);
|
|
656
|
-
if (initialData?.metadata) {
|
|
657
|
-
applyMetadata(initialData.metadata);
|
|
658
|
-
}
|
|
659
|
-
hydrateRoot(
|
|
660
|
-
container,
|
|
661
|
-
/* @__PURE__ */ jsx3(
|
|
662
|
-
AppShell,
|
|
663
|
-
{
|
|
664
|
-
initialState,
|
|
665
|
-
routes,
|
|
666
|
-
notFoundRoute,
|
|
667
|
-
errorRoute
|
|
668
|
-
}
|
|
669
|
-
)
|
|
670
|
-
);
|
|
671
1099
|
} catch (error) {
|
|
672
|
-
console.error(
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
error
|
|
676
|
-
|
|
1100
|
+
console.error("\n\u274C [client] Fatal error during bootstrap:");
|
|
1101
|
+
console.error(error);
|
|
1102
|
+
if (error instanceof Error) {
|
|
1103
|
+
console.error("\nError details:");
|
|
1104
|
+
console.error(` Message: ${error.message}`);
|
|
1105
|
+
if (error.stack) {
|
|
1106
|
+
console.error(` Stack: ${error.stack}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
|
|
677
1110
|
window.location.reload();
|
|
678
1111
|
}
|
|
679
1112
|
})();
|