@sigil-dev/grimoire 0.7.7 → 0.8.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/package.json +5 -5
- package/src/client/router.ts +291 -290
- package/src/rendering/hydrate.ts +119 -120
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.8.0",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.ts",
|
|
9
9
|
"./server": "./server.ts",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"vite": "^8.0.16"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@sigil-dev/compiler": "0.
|
|
36
|
-
"@sigil-dev/runtime": "0.
|
|
35
|
+
"@sigil-dev/compiler": "0.8.0",
|
|
36
|
+
"@sigil-dev/runtime": "0.8.0",
|
|
37
37
|
"typescript": "^5"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "latest",
|
|
41
|
-
"@sigil-dev/compiler": "0.
|
|
42
|
-
"@sigil-dev/runtime": "0.
|
|
41
|
+
"@sigil-dev/compiler": "0.8.0",
|
|
42
|
+
"@sigil-dev/runtime": "0.8.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/client/router.ts
CHANGED
|
@@ -1,290 +1,291 @@
|
|
|
1
|
-
import { popHydrationNodes, pushHydrationNodes } from "@sigil-dev/runtime";
|
|
2
|
-
import { withEffectScope } from "./scope";
|
|
3
|
-
|
|
4
|
-
let routeMap: Record<
|
|
5
|
-
string,
|
|
6
|
-
((props: any) => any) & { load?: (ctx: any) => Promise<any> }
|
|
7
|
-
> = {};
|
|
8
|
-
let layoutList: { path: string; component: (props: any) => any }[] = [];
|
|
9
|
-
let disposeCurrentPage: (() => void) | null = null;
|
|
10
|
-
let currentPath = location.pathname;
|
|
11
|
-
|
|
12
|
-
// C1: Navigation hooks
|
|
13
|
-
let beforeNavigateCb: ((url: URL) => boolean | void) | null = null;
|
|
14
|
-
let onNavigateCb: ((url: URL) => void) | null = null;
|
|
15
|
-
let afterNavigateCb: ((url: URL) => void) | null = null;
|
|
16
|
-
|
|
17
|
-
// // HMR
|
|
18
|
-
// (globalThis as any).__grimoire_routes__ = routeMap;
|
|
19
|
-
// (globalThis as any).__grimoire_current_pattern__ = currentPath;
|
|
20
|
-
// (globalThis as any).__grimoire_navigate__ = navigate;
|
|
21
|
-
|
|
22
|
-
export async function hmrRerender(): Promise<void> {
|
|
23
|
-
if (!lastState) {
|
|
24
|
-
location.reload();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
disposeCurrentPage?.();
|
|
28
|
-
pushHydrationNodes([]);
|
|
29
|
-
let rootNode: any;
|
|
30
|
-
disposeCurrentPage = withEffectScope(() => {
|
|
31
|
-
rootNode = buildRouteTree(lastState!);
|
|
32
|
-
});
|
|
33
|
-
popHydrationNodes();
|
|
34
|
-
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// (globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
38
|
-
|
|
39
|
-
export function beforeNavigate(cb: (url: URL) => boolean | void): void {
|
|
40
|
-
beforeNavigateCb = cb;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function onNavigate(cb: (url: URL) => void): void {
|
|
44
|
-
onNavigateCb = cb;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function afterNavigate(cb: (url: URL) => void): void {
|
|
48
|
-
afterNavigateCb = cb;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// C2: Scroll restoration
|
|
52
|
-
const scrollPositions = new Map<string, number>();
|
|
53
|
-
|
|
54
|
-
function saveScrollPosition(): void {
|
|
55
|
-
scrollPositions.set(currentPath, window.scrollY);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function restoreScrollPosition(path: string): void {
|
|
59
|
-
const saved = scrollPositions.get(path);
|
|
60
|
-
if (saved !== undefined) {
|
|
61
|
-
requestAnimationFrame(() => window.scrollTo(0, saved));
|
|
62
|
-
} else {
|
|
63
|
-
requestAnimationFrame(() => window.scrollTo(0, 0));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// C3: Preloading
|
|
68
|
-
let preloadTimer: Timer | null = null;
|
|
69
|
-
|
|
70
|
-
function setupPreload(): void {
|
|
71
|
-
document.addEventListener("mouseover", (e) => {
|
|
72
|
-
const a = (e.target as Element).closest("a");
|
|
73
|
-
if (!a) return;
|
|
74
|
-
const href = a.getAttribute("href");
|
|
75
|
-
if (!href) return;
|
|
76
|
-
if (/^(https?:\/\/|\/\/|#|mailto:|tel:)/.test(href)) return;
|
|
77
|
-
|
|
78
|
-
const preloadData = a.hasAttribute("data-sigil-preload-data");
|
|
79
|
-
const preloadCode = a.hasAttribute("data-sigil-preload-code");
|
|
80
|
-
if (!preloadData && !preloadCode) return;
|
|
81
|
-
|
|
82
|
-
if (preloadTimer) clearTimeout(preloadTimer);
|
|
83
|
-
preloadTimer = setTimeout(() => {
|
|
84
|
-
if (preloadCode) {
|
|
85
|
-
const link = document.createElement("link");
|
|
86
|
-
link.rel = "prefetch";
|
|
87
|
-
link.href = href;
|
|
88
|
-
document.head.appendChild(link);
|
|
89
|
-
}
|
|
90
|
-
if (preloadData) {
|
|
91
|
-
fetch(href, {
|
|
92
|
-
headers: { "x-grimoire-navigate": "1" },
|
|
93
|
-
priority: "low",
|
|
94
|
-
}).catch(() => {});
|
|
95
|
-
}
|
|
96
|
-
}, 80);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
document.addEventListener("mouseout", (e) => {
|
|
100
|
-
const a = (e.target as Element).closest("a");
|
|
101
|
-
if (!a) return;
|
|
102
|
-
if (preloadTimer) clearTimeout(preloadTimer);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let lastState: {
|
|
107
|
-
data: any;
|
|
108
|
-
layoutData: any[];
|
|
109
|
-
params: any;
|
|
110
|
-
pattern: string;
|
|
111
|
-
} | null = null;
|
|
112
|
-
|
|
113
|
-
function buildRouteTree(state: typeof lastState): any {
|
|
114
|
-
const { data, layoutData, params, pattern } = state!;
|
|
115
|
-
const Page = routeMap[pattern];
|
|
116
|
-
if (!Page) throw new Error(`no component for pattern ${pattern}`);
|
|
117
|
-
|
|
118
|
-
const matchedLayouts = layoutList
|
|
119
|
-
.filter(
|
|
120
|
-
(l) =>
|
|
121
|
-
l.path === "/" ||
|
|
122
|
-
pattern === l.path ||
|
|
123
|
-
pattern.startsWith(l.path + "/"),
|
|
124
|
-
)
|
|
125
|
-
.sort((a, b) => a.path.length - b.path.length);
|
|
126
|
-
|
|
127
|
-
let renderFn = () => {
|
|
128
|
-
const pageNode = Page({ data, params });
|
|
129
|
-
const pageDiv = document.createElement("div");
|
|
130
|
-
pageDiv.id = "grimoire-page";
|
|
131
|
-
pageDiv.appendChild(pageNode);
|
|
132
|
-
return pageDiv;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
136
|
-
const LayoutComponent = matchedLayouts[i].component;
|
|
137
|
-
const innerRender = renderFn;
|
|
138
|
-
const currentLayoutData = layoutData?.[i];
|
|
139
|
-
renderFn = () => {
|
|
140
|
-
const childNode = innerRender();
|
|
141
|
-
return LayoutComponent({
|
|
142
|
-
data: currentLayoutData,
|
|
143
|
-
params,
|
|
144
|
-
children: childNode,
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return renderFn();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function navigate(path: string) {
|
|
153
|
-
const url = new URL(path, location.origin);
|
|
154
|
-
|
|
155
|
-
if (beforeNavigateCb) {
|
|
156
|
-
const shouldProceed = beforeNavigateCb(url);
|
|
157
|
-
if (shouldProceed === false) return;
|
|
158
|
-
}
|
|
159
|
-
onNavigateCb?.(url);
|
|
160
|
-
saveScrollPosition();
|
|
161
|
-
|
|
162
|
-
const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
|
|
163
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
164
|
-
if (!contentType.includes("application/json")) {
|
|
165
|
-
window.location.href = path;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const json = await res.json();
|
|
169
|
-
const { data, layoutData, params, pattern, head } = json;
|
|
170
|
-
const Page = routeMap[pattern];
|
|
171
|
-
if (!Page) {
|
|
172
|
-
window.location.href = path;
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let mergedData = data;
|
|
177
|
-
if (Page.load) {
|
|
178
|
-
try {
|
|
179
|
-
const clientData = await Page.load({
|
|
180
|
-
params,
|
|
181
|
-
url,
|
|
182
|
-
request: new Request(url),
|
|
183
|
-
});
|
|
184
|
-
mergedData = { ...clientData, ...data };
|
|
185
|
-
} catch (e: any) {
|
|
186
|
-
if (e?.location) {
|
|
187
|
-
window.location.href = e.location;
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
console.error("[grimoire] universal load error:", e);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// track state for HMR rerender
|
|
195
|
-
lastState = { data: mergedData, layoutData, params, pattern };
|
|
196
|
-
(globalThis as any).__grimoire_current_pattern__ = pattern;
|
|
197
|
-
|
|
198
|
-
disposeCurrentPage?.();
|
|
199
|
-
disposeCurrentPage = null;
|
|
200
|
-
pushHydrationNodes([]);
|
|
201
|
-
let rootNode: any;
|
|
202
|
-
disposeCurrentPage = withEffectScope(() => {
|
|
203
|
-
try {
|
|
204
|
-
rootNode = buildRouteTree(lastState!);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
console.error(e);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
popHydrationNodes();
|
|
210
|
-
|
|
211
|
-
updateHead(head);
|
|
212
|
-
document.title = head?.title ?? document.title;
|
|
213
|
-
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
214
|
-
currentPath = path;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
(globalThis as any).
|
|
244
|
-
(globalThis as any).
|
|
245
|
-
(globalThis as any).
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
document.title
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
(el
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1
|
+
import { popHydrationNodes, pushHydrationNodes } from "@sigil-dev/runtime";
|
|
2
|
+
import { withEffectScope } from "./scope";
|
|
3
|
+
|
|
4
|
+
let routeMap: Record<
|
|
5
|
+
string,
|
|
6
|
+
((props: any) => any) & { load?: (ctx: any) => Promise<any> }
|
|
7
|
+
> = {};
|
|
8
|
+
let layoutList: { path: string; component: (props: any) => any }[] = [];
|
|
9
|
+
let disposeCurrentPage: (() => void) | null = null;
|
|
10
|
+
let currentPath = location.pathname;
|
|
11
|
+
|
|
12
|
+
// C1: Navigation hooks
|
|
13
|
+
let beforeNavigateCb: ((url: URL) => boolean | void) | null = null;
|
|
14
|
+
let onNavigateCb: ((url: URL) => void) | null = null;
|
|
15
|
+
let afterNavigateCb: ((url: URL) => void) | null = null;
|
|
16
|
+
|
|
17
|
+
// // HMR
|
|
18
|
+
// (globalThis as any).__grimoire_routes__ = routeMap;
|
|
19
|
+
// (globalThis as any).__grimoire_current_pattern__ = currentPath;
|
|
20
|
+
// (globalThis as any).__grimoire_navigate__ = navigate;
|
|
21
|
+
|
|
22
|
+
export async function hmrRerender(): Promise<void> {
|
|
23
|
+
if (!lastState) {
|
|
24
|
+
location.reload();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
disposeCurrentPage?.();
|
|
28
|
+
pushHydrationNodes([]);
|
|
29
|
+
let rootNode: any;
|
|
30
|
+
disposeCurrentPage = withEffectScope(() => {
|
|
31
|
+
rootNode = buildRouteTree(lastState!);
|
|
32
|
+
});
|
|
33
|
+
popHydrationNodes();
|
|
34
|
+
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// (globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
38
|
+
|
|
39
|
+
export function beforeNavigate(cb: (url: URL) => boolean | void): void {
|
|
40
|
+
beforeNavigateCb = cb;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function onNavigate(cb: (url: URL) => void): void {
|
|
44
|
+
onNavigateCb = cb;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function afterNavigate(cb: (url: URL) => void): void {
|
|
48
|
+
afterNavigateCb = cb;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// C2: Scroll restoration
|
|
52
|
+
const scrollPositions = new Map<string, number>();
|
|
53
|
+
|
|
54
|
+
function saveScrollPosition(): void {
|
|
55
|
+
scrollPositions.set(currentPath, window.scrollY);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function restoreScrollPosition(path: string): void {
|
|
59
|
+
const saved = scrollPositions.get(path);
|
|
60
|
+
if (saved !== undefined) {
|
|
61
|
+
requestAnimationFrame(() => window.scrollTo(0, saved));
|
|
62
|
+
} else {
|
|
63
|
+
requestAnimationFrame(() => window.scrollTo(0, 0));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// C3: Preloading
|
|
68
|
+
let preloadTimer: Timer | null = null;
|
|
69
|
+
|
|
70
|
+
function setupPreload(): void {
|
|
71
|
+
document.addEventListener("mouseover", (e) => {
|
|
72
|
+
const a = (e.target as Element).closest("a");
|
|
73
|
+
if (!a) return;
|
|
74
|
+
const href = a.getAttribute("href");
|
|
75
|
+
if (!href) return;
|
|
76
|
+
if (/^(https?:\/\/|\/\/|#|mailto:|tel:)/.test(href)) return;
|
|
77
|
+
|
|
78
|
+
const preloadData = a.hasAttribute("data-sigil-preload-data");
|
|
79
|
+
const preloadCode = a.hasAttribute("data-sigil-preload-code");
|
|
80
|
+
if (!preloadData && !preloadCode) return;
|
|
81
|
+
|
|
82
|
+
if (preloadTimer) clearTimeout(preloadTimer);
|
|
83
|
+
preloadTimer = setTimeout(() => {
|
|
84
|
+
if (preloadCode) {
|
|
85
|
+
const link = document.createElement("link");
|
|
86
|
+
link.rel = "prefetch";
|
|
87
|
+
link.href = href;
|
|
88
|
+
document.head.appendChild(link);
|
|
89
|
+
}
|
|
90
|
+
if (preloadData) {
|
|
91
|
+
fetch(href, {
|
|
92
|
+
headers: { "x-grimoire-navigate": "1" },
|
|
93
|
+
priority: "low",
|
|
94
|
+
}).catch(() => {});
|
|
95
|
+
}
|
|
96
|
+
}, 80);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
document.addEventListener("mouseout", (e) => {
|
|
100
|
+
const a = (e.target as Element).closest("a");
|
|
101
|
+
if (!a) return;
|
|
102
|
+
if (preloadTimer) clearTimeout(preloadTimer);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let lastState: {
|
|
107
|
+
data: any;
|
|
108
|
+
layoutData: any[];
|
|
109
|
+
params: any;
|
|
110
|
+
pattern: string;
|
|
111
|
+
} | null = null;
|
|
112
|
+
|
|
113
|
+
function buildRouteTree(state: typeof lastState): any {
|
|
114
|
+
const { data, layoutData, params, pattern } = state!;
|
|
115
|
+
const Page = routeMap[pattern];
|
|
116
|
+
if (!Page) throw new Error(`no component for pattern ${pattern}`);
|
|
117
|
+
|
|
118
|
+
const matchedLayouts = layoutList
|
|
119
|
+
.filter(
|
|
120
|
+
(l) =>
|
|
121
|
+
l.path === "/" ||
|
|
122
|
+
pattern === l.path ||
|
|
123
|
+
pattern.startsWith(l.path + "/"),
|
|
124
|
+
)
|
|
125
|
+
.sort((a, b) => a.path.length - b.path.length);
|
|
126
|
+
|
|
127
|
+
let renderFn = () => {
|
|
128
|
+
const pageNode = Page({ data, params });
|
|
129
|
+
const pageDiv = document.createElement("div");
|
|
130
|
+
pageDiv.id = "grimoire-page";
|
|
131
|
+
pageDiv.appendChild(pageNode);
|
|
132
|
+
return pageDiv;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
136
|
+
const LayoutComponent = matchedLayouts[i].component;
|
|
137
|
+
const innerRender = renderFn;
|
|
138
|
+
const currentLayoutData = layoutData?.[i];
|
|
139
|
+
renderFn = () => {
|
|
140
|
+
const childNode = innerRender();
|
|
141
|
+
return LayoutComponent({
|
|
142
|
+
data: currentLayoutData,
|
|
143
|
+
params,
|
|
144
|
+
children: childNode,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return renderFn();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function navigate(path: string) {
|
|
153
|
+
const url = new URL(path, location.origin);
|
|
154
|
+
|
|
155
|
+
if (beforeNavigateCb) {
|
|
156
|
+
const shouldProceed = beforeNavigateCb(url);
|
|
157
|
+
if (shouldProceed === false) return;
|
|
158
|
+
}
|
|
159
|
+
onNavigateCb?.(url);
|
|
160
|
+
saveScrollPosition();
|
|
161
|
+
|
|
162
|
+
const res = await fetch(path, { headers: { "x-grimoire-navigate": "1" } });
|
|
163
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
164
|
+
if (!contentType.includes("application/json")) {
|
|
165
|
+
window.location.href = path;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const json = await res.json();
|
|
169
|
+
const { data, layoutData, params, pattern, head } = json;
|
|
170
|
+
const Page = routeMap[pattern];
|
|
171
|
+
if (!Page) {
|
|
172
|
+
window.location.href = path;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let mergedData = data;
|
|
177
|
+
if (Page.load) {
|
|
178
|
+
try {
|
|
179
|
+
const clientData = await Page.load({
|
|
180
|
+
params,
|
|
181
|
+
url,
|
|
182
|
+
request: new Request(url),
|
|
183
|
+
});
|
|
184
|
+
mergedData = { ...clientData, ...data };
|
|
185
|
+
} catch (e: any) {
|
|
186
|
+
if (e?.location) {
|
|
187
|
+
window.location.href = e.location;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.error("[grimoire] universal load error:", e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// track state for HMR rerender
|
|
195
|
+
lastState = { data: mergedData, layoutData, params, pattern };
|
|
196
|
+
(globalThis as any).__grimoire_current_pattern__ = pattern;
|
|
197
|
+
|
|
198
|
+
disposeCurrentPage?.();
|
|
199
|
+
disposeCurrentPage = null;
|
|
200
|
+
pushHydrationNodes([]);
|
|
201
|
+
let rootNode: any;
|
|
202
|
+
disposeCurrentPage = withEffectScope(() => {
|
|
203
|
+
try {
|
|
204
|
+
rootNode = buildRouteTree(lastState!);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error(e);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
popHydrationNodes();
|
|
210
|
+
|
|
211
|
+
updateHead(head);
|
|
212
|
+
document.title = head?.title ?? document.title;
|
|
213
|
+
document.getElementById("grimoire-root")?.replaceChildren(rootNode);
|
|
214
|
+
currentPath = path;
|
|
215
|
+
history.pushState(null, "", path);
|
|
216
|
+
restoreScrollPosition(path);
|
|
217
|
+
afterNavigateCb?.(url);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function initRouter(
|
|
221
|
+
routes: Record<
|
|
222
|
+
string,
|
|
223
|
+
((props: any) => any) & { load?: (ctx: any) => Promise<any> }
|
|
224
|
+
>,
|
|
225
|
+
layouts: any[],
|
|
226
|
+
initialDispose?: () => void,
|
|
227
|
+
) {
|
|
228
|
+
routeMap = routes;
|
|
229
|
+
layoutList = layouts;
|
|
230
|
+
disposeCurrentPage = initialDispose ?? null;
|
|
231
|
+
currentPath = location.pathname;
|
|
232
|
+
|
|
233
|
+
// set initial HMR state from SSR
|
|
234
|
+
const stateEl = document.getElementById("__grimoire_state__");
|
|
235
|
+
if (stateEl) {
|
|
236
|
+
const state = JSON.parse(stateEl.textContent!);
|
|
237
|
+
lastState = {
|
|
238
|
+
data: state.data,
|
|
239
|
+
layoutData: state.layoutData,
|
|
240
|
+
params: state.params,
|
|
241
|
+
pattern: state.pattern,
|
|
242
|
+
};
|
|
243
|
+
(globalThis as any).__grimoire_current_pattern__ = state.pattern;
|
|
244
|
+
(globalThis as any).__grimoire_routes__ = routeMap;
|
|
245
|
+
(globalThis as any).__grimoire_rerender__ = hmrRerender;
|
|
246
|
+
(globalThis as any).__grimoire_navigate__ = (path: string) =>
|
|
247
|
+
navigate(path);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
saveScrollPosition();
|
|
251
|
+
document.addEventListener("click", handleClick);
|
|
252
|
+
window.addEventListener("popstate", () => {
|
|
253
|
+
if (location.pathname !== currentPath) {
|
|
254
|
+
saveScrollPosition();
|
|
255
|
+
currentPath = location.pathname;
|
|
256
|
+
navigate(location.pathname);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
setupPreload();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function updateHead(headHtml: string) {
|
|
263
|
+
document
|
|
264
|
+
.querySelectorAll("[data-grimoire-head]")
|
|
265
|
+
//biome-ignore lint/suspicious/useIterableCallbackReturn: shut up
|
|
266
|
+
.forEach((el) => el.remove());
|
|
267
|
+
const parsed = headHtml
|
|
268
|
+
? new DOMParser().parseFromString(`<head>${headHtml}</head>`, "text/html")
|
|
269
|
+
: null;
|
|
270
|
+
const incomingTitle = parsed?.querySelector("title");
|
|
271
|
+
//biome-ignore lint/suspicious/useIterableCallbackReturn: shut up
|
|
272
|
+
document.querySelectorAll("title").forEach((el) => el.remove());
|
|
273
|
+
document.title = incomingTitle?.textContent ?? document.title;
|
|
274
|
+
if (parsed) {
|
|
275
|
+
for (const el of Array.from(parsed.head.children)) {
|
|
276
|
+
if (el.tagName === "TITLE") continue;
|
|
277
|
+
(el as HTMLElement).dataset.grimoireHead = "1";
|
|
278
|
+
document.head.appendChild(el);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleClick(e: MouseEvent) {
|
|
284
|
+
const a = (e.target as Element).closest("a");
|
|
285
|
+
if (!a) return;
|
|
286
|
+
const href = a.getAttribute("href");
|
|
287
|
+
if (!href) return;
|
|
288
|
+
if (/^(https?:\/\/|\/\/|#|mailto:|tel:)/.test(href)) return;
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
navigate(href);
|
|
291
|
+
}
|
package/src/rendering/hydrate.ts
CHANGED
|
@@ -1,120 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "../client/router";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(globalThis as any).
|
|
23
|
-
(globalThis as any).
|
|
24
|
-
(globalThis as any).
|
|
25
|
-
(globalThis as any).
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
state.pattern
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
initRouter(routes, layouts, initialDispose);
|
|
1
|
+
import {
|
|
2
|
+
claim,
|
|
3
|
+
getHydrationNodes,
|
|
4
|
+
insert,
|
|
5
|
+
popHydrationNodes,
|
|
6
|
+
pushHydrationNodes,
|
|
7
|
+
} from "@sigil-dev/runtime";
|
|
8
|
+
//@ts-expect-error compiler generated
|
|
9
|
+
import { layouts, routes } from "#grimoire-routes";
|
|
10
|
+
import {
|
|
11
|
+
afterNavigate,
|
|
12
|
+
beforeNavigate,
|
|
13
|
+
navigate,
|
|
14
|
+
onNavigate,
|
|
15
|
+
} from "../client/router";
|
|
16
|
+
import { initRouter } from "../client/router.ts";
|
|
17
|
+
import { withEffectScope } from "../client/scope.ts";
|
|
18
|
+
import { Head } from "./head";
|
|
19
|
+
|
|
20
|
+
// expose for HMR module shim
|
|
21
|
+
(globalThis as any).__grimoire_Head__ = Head;
|
|
22
|
+
(globalThis as any).__grimoire_navigate__ = navigate;
|
|
23
|
+
(globalThis as any).__grimoire_beforeNavigate__ = beforeNavigate;
|
|
24
|
+
(globalThis as any).__grimoire_onNavigate__ = onNavigate;
|
|
25
|
+
(globalThis as any).__grimoire_afterNavigate__ = afterNavigate;
|
|
26
|
+
|
|
27
|
+
const stateEl = document.getElementById("__grimoire_state__");
|
|
28
|
+
let initialDispose: (() => void) | undefined;
|
|
29
|
+
|
|
30
|
+
if (stateEl) {
|
|
31
|
+
const state = JSON.parse(stateEl.textContent!);
|
|
32
|
+
const Page = routes[state.pattern];
|
|
33
|
+
if (Page) {
|
|
34
|
+
const slot = document.getElementById("grimoire-root");
|
|
35
|
+
if (slot) {
|
|
36
|
+
const matchedLayouts = layouts
|
|
37
|
+
.filter(
|
|
38
|
+
(l: any) =>
|
|
39
|
+
l.path === "/" ||
|
|
40
|
+
state.pattern === l.path ||
|
|
41
|
+
state.pattern.startsWith(l.path + "/"),
|
|
42
|
+
)
|
|
43
|
+
.sort((a: any, b: any) => a.path.length - b.path.length);
|
|
44
|
+
|
|
45
|
+
// When layouts are involved, nested <!--g--> delimiters in the flat SSR pool
|
|
46
|
+
// cause anchor-mount to claim the wrong anchor and remove layout DOM nodes.
|
|
47
|
+
// Clear SSR content and re-render in DOM mode (empty pool) instead.
|
|
48
|
+
const hasLayouts = matchedLayouts.length > 0;
|
|
49
|
+
if (hasLayouts) {
|
|
50
|
+
slot.replaceChildren();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ssrClones = hasLayouts
|
|
54
|
+
? []
|
|
55
|
+
: Array.from(slot.childNodes).map((n) => n.cloneNode(true));
|
|
56
|
+
pushHydrationNodes(
|
|
57
|
+
hasLayouts ? [] : (Array.from(slot.childNodes) as ChildNode[]),
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
initialDispose = withEffectScope(() => {
|
|
61
|
+
try {
|
|
62
|
+
let renderFn = () => {
|
|
63
|
+
const pageDiv = claim(getHydrationNodes(), "div");
|
|
64
|
+
pageDiv.id = "grimoire-page";
|
|
65
|
+
|
|
66
|
+
if (pageDiv.childNodes.length > 0) {
|
|
67
|
+
pushHydrationNodes(
|
|
68
|
+
Array.from(pageDiv.childNodes) as ChildNode[],
|
|
69
|
+
);
|
|
70
|
+
const pageNode = Page({
|
|
71
|
+
data: state.data,
|
|
72
|
+
params: state.params,
|
|
73
|
+
});
|
|
74
|
+
popHydrationNodes();
|
|
75
|
+
insert(pageDiv, pageNode);
|
|
76
|
+
} else {
|
|
77
|
+
const pageNode = Page({
|
|
78
|
+
data: state.data,
|
|
79
|
+
params: state.params,
|
|
80
|
+
});
|
|
81
|
+
insert(pageDiv, pageNode);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return pageDiv;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (let i = matchedLayouts.length - 1; i >= 0; i--) {
|
|
88
|
+
const LayoutComponent = matchedLayouts[i].component;
|
|
89
|
+
const innerRender = renderFn;
|
|
90
|
+
const layoutData = state.layoutData?.[i];
|
|
91
|
+
renderFn = () => {
|
|
92
|
+
// Pre-render inner content so children is a Node (insert() doesn't call functions)
|
|
93
|
+
const childNode = innerRender();
|
|
94
|
+
return LayoutComponent({
|
|
95
|
+
data: layoutData,
|
|
96
|
+
params: state.params,
|
|
97
|
+
children: childNode,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rootNode = renderFn();
|
|
103
|
+
insert(slot, rootNode);
|
|
104
|
+
} finally {
|
|
105
|
+
popHydrationNodes();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn("[grimoire] hydration error:", e);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!slot.hasChildNodes() && ssrClones.length > 0) {
|
|
113
|
+
slot.replaceChildren(...ssrClones);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
initRouter(routes, layouts, initialDispose);
|