@void/react 0.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/README.md +216 -0
- package/dist/action-BFWtavbf.mjs +29 -0
- package/dist/context-BCeFV8Jy.mjs +9 -0
- package/dist/index-6hxGVqsE.d.mts +125 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +4 -0
- package/dist/plugin.d.mts +16 -0
- package/dist/plugin.mjs +553 -0
- package/dist/runtime/index.d.mts +2 -0
- package/dist/runtime/index.mjs +4 -0
- package/dist/runtime/pages-client.d.mts +27 -0
- package/dist/runtime/pages-client.mjs +417 -0
- package/dist/runtime/pages-server.d.mts +27 -0
- package/dist/runtime/pages-server.mjs +112 -0
- package/dist/runtime/prefetch.d.mts +2 -0
- package/dist/runtime/prefetch.mjs +2 -0
- package/dist/runtime-C24S_Dlg.mjs +492 -0
- package/package.json +62 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { i as SharedContext, n as NavigationContext, r as RouterContext, t as ErrorsContext } from "../context-BCeFV8Jy.mjs";
|
|
2
|
+
import { n as setActionRouter } from "../action-BFWtavbf.mjs";
|
|
3
|
+
import React, { Suspense, use, useCallback, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useRef, useTransition } from "react";
|
|
4
|
+
import { createPageNavigationEngine, createRouterFacade, idleNavigationState, isAbortError, isCallSiteActionError } from "void/pages-client";
|
|
5
|
+
import { hydrateRoot } from "react-dom/client";
|
|
6
|
+
//#region src/runtime/pages-client.ts
|
|
7
|
+
const deferredState = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const viewTransitionWaiters = /* @__PURE__ */ new Map();
|
|
9
|
+
const pendingVisitSettlers = /* @__PURE__ */ new Map();
|
|
10
|
+
const committedResources = /* @__PURE__ */ new Set();
|
|
11
|
+
let activePendingNavigationId = null;
|
|
12
|
+
let scheduleVisit = null;
|
|
13
|
+
function createDeferredResource() {
|
|
14
|
+
let resolve;
|
|
15
|
+
let reject;
|
|
16
|
+
const promise = new Promise((res, rej) => {
|
|
17
|
+
resolve = res;
|
|
18
|
+
reject = rej;
|
|
19
|
+
});
|
|
20
|
+
promise.catch(() => {});
|
|
21
|
+
deferredState.set(promise, {
|
|
22
|
+
status: "pending",
|
|
23
|
+
resolve,
|
|
24
|
+
reject
|
|
25
|
+
});
|
|
26
|
+
return promise;
|
|
27
|
+
}
|
|
28
|
+
function resolveDeferredResource(ref, value) {
|
|
29
|
+
const state = deferredState.get(ref);
|
|
30
|
+
if (state?.status === "pending") {
|
|
31
|
+
state.resolve(value);
|
|
32
|
+
deferredState.set(ref, { status: "resolved" });
|
|
33
|
+
}
|
|
34
|
+
return ref;
|
|
35
|
+
}
|
|
36
|
+
function rejectDeferredResource(ref, error) {
|
|
37
|
+
const state = deferredState.get(ref);
|
|
38
|
+
if (state?.status === "pending") {
|
|
39
|
+
state.reject(new Error(error));
|
|
40
|
+
deferredState.set(ref, { status: "rejected" });
|
|
41
|
+
}
|
|
42
|
+
return ref;
|
|
43
|
+
}
|
|
44
|
+
function isDeferredResourceLoading(ref) {
|
|
45
|
+
return deferredState.get(ref)?.status === "pending";
|
|
46
|
+
}
|
|
47
|
+
function currentUrl() {
|
|
48
|
+
return window.location.pathname + window.location.search + window.location.hash;
|
|
49
|
+
}
|
|
50
|
+
function normalizeUrl(value) {
|
|
51
|
+
if (typeof value === "string" && value.startsWith("#")) return value;
|
|
52
|
+
const url = value instanceof URL ? value : new URL(value, window.location.origin);
|
|
53
|
+
return url.origin === window.location.origin ? url.pathname + url.search + url.hash : url.href;
|
|
54
|
+
}
|
|
55
|
+
function navigationForResource(resource) {
|
|
56
|
+
const method = resource.method.toUpperCase();
|
|
57
|
+
return {
|
|
58
|
+
state: method !== "GET" ? "submitting" : "loading",
|
|
59
|
+
location: new URL(resource.fetchUrl + resource.hash, window.location.origin),
|
|
60
|
+
method: method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE" ? method : "GET"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function appReducer(state, action) {
|
|
64
|
+
switch (action.type) {
|
|
65
|
+
case "begin": return {
|
|
66
|
+
resource: state.resource,
|
|
67
|
+
rollbackResource: state.resource,
|
|
68
|
+
pendingId: action.resource.id,
|
|
69
|
+
routeUrl: state.routeUrl,
|
|
70
|
+
navigation: action.navigation
|
|
71
|
+
};
|
|
72
|
+
case "reveal":
|
|
73
|
+
if (state.pendingId !== action.resource.id) return state;
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
resource: action.resource
|
|
77
|
+
};
|
|
78
|
+
case "finish":
|
|
79
|
+
if (state.pendingId !== action.resource.id) return state;
|
|
80
|
+
return {
|
|
81
|
+
resource: action.resource.immediate ? state.resource : action.resource,
|
|
82
|
+
rollbackResource: null,
|
|
83
|
+
pendingId: null,
|
|
84
|
+
routeUrl: action.location,
|
|
85
|
+
navigation: idleNavigationState()
|
|
86
|
+
};
|
|
87
|
+
case "fail":
|
|
88
|
+
if (state.pendingId !== action.resource.id) return state;
|
|
89
|
+
return {
|
|
90
|
+
...state,
|
|
91
|
+
rollbackResource: null,
|
|
92
|
+
pendingId: null,
|
|
93
|
+
routeUrl: action.location,
|
|
94
|
+
navigation: idleNavigationState()
|
|
95
|
+
};
|
|
96
|
+
case "abandon":
|
|
97
|
+
if (state.pendingId !== action.resource.id) return state;
|
|
98
|
+
return {
|
|
99
|
+
resource: state.rollbackResource || state.resource,
|
|
100
|
+
rollbackResource: null,
|
|
101
|
+
pendingId: null,
|
|
102
|
+
routeUrl: action.location,
|
|
103
|
+
navigation: idleNavigationState()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function createRouter(engine) {
|
|
108
|
+
const router = {
|
|
109
|
+
_hoverDelay: engine._hoverDelay,
|
|
110
|
+
get url() {
|
|
111
|
+
return new URL(currentUrl(), window.location.origin);
|
|
112
|
+
},
|
|
113
|
+
get path() {
|
|
114
|
+
return this.url.pathname;
|
|
115
|
+
},
|
|
116
|
+
get query() {
|
|
117
|
+
return this.url.searchParams;
|
|
118
|
+
},
|
|
119
|
+
visit(url, options = {}) {
|
|
120
|
+
if (!scheduleVisit) return Promise.reject(/* @__PURE__ */ new Error("router.visit() called before router initialized"));
|
|
121
|
+
return scheduleVisit(normalizeUrl(url), options);
|
|
122
|
+
},
|
|
123
|
+
refresh(options = {}) {
|
|
124
|
+
return router.visit(window.location.pathname + window.location.search, {
|
|
125
|
+
preserveState: true,
|
|
126
|
+
preserveScroll: true,
|
|
127
|
+
...options
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
prefetch(url, options = {}) {
|
|
131
|
+
return engine.prefetch(normalizeUrl(url), options);
|
|
132
|
+
},
|
|
133
|
+
flush(url, method = "GET") {
|
|
134
|
+
engine.flush(normalizeUrl(url), method);
|
|
135
|
+
},
|
|
136
|
+
flushAll() {
|
|
137
|
+
engine.flushAll();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return router;
|
|
141
|
+
}
|
|
142
|
+
function RouteResource({ resource, onCommit }) {
|
|
143
|
+
const result = use(resource.shell);
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
if (result) onCommit(resource, result);
|
|
146
|
+
}, [
|
|
147
|
+
onCommit,
|
|
148
|
+
resource,
|
|
149
|
+
result
|
|
150
|
+
]);
|
|
151
|
+
if (!result) return null;
|
|
152
|
+
const pageData = result.pageData;
|
|
153
|
+
const PageComponent = result.component;
|
|
154
|
+
let element = React.createElement(PageComponent, pageData.props);
|
|
155
|
+
for (let i = result.layouts.length - 1; i >= 0; i--) {
|
|
156
|
+
const Layout = result.layouts[i];
|
|
157
|
+
element = React.createElement(Layout, null, element);
|
|
158
|
+
}
|
|
159
|
+
return React.createElement(SharedContext.Provider, { value: pageData.shared }, React.createElement(ErrorsContext.Provider, { value: pageData.errors ?? {} }, element));
|
|
160
|
+
}
|
|
161
|
+
function startViewTransition(resource, reveal, enabled) {
|
|
162
|
+
if (!enabled || typeof document.startViewTransition !== "function") {
|
|
163
|
+
reveal();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let resolveViewTransition;
|
|
167
|
+
const readyForCapture = new Promise((resolve) => {
|
|
168
|
+
resolveViewTransition = resolve;
|
|
169
|
+
});
|
|
170
|
+
viewTransitionWaiters.set(resource.id, resolveViewTransition);
|
|
171
|
+
document.startViewTransition(() => {
|
|
172
|
+
reveal();
|
|
173
|
+
return readyForCapture;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function App({ engine, router, viewTransitions }) {
|
|
177
|
+
const initialPageData = engine.initialPageData;
|
|
178
|
+
const sharedStateRef = useRef(initialPageData.shared);
|
|
179
|
+
const [state, dispatch] = useReducer(appReducer, {
|
|
180
|
+
resource: useMemo(() => ({
|
|
181
|
+
id: 0,
|
|
182
|
+
url: initialPageData.url || currentUrl(),
|
|
183
|
+
fetchUrl: initialPageData.url || currentUrl(),
|
|
184
|
+
hash: window.location.hash,
|
|
185
|
+
method: "GET",
|
|
186
|
+
options: {},
|
|
187
|
+
immediate: false,
|
|
188
|
+
shell: Promise.resolve({
|
|
189
|
+
pageData: {
|
|
190
|
+
...initialPageData,
|
|
191
|
+
shared: initialPageData.shared
|
|
192
|
+
},
|
|
193
|
+
component: engine.initialComponent,
|
|
194
|
+
layouts: engine.initialLayouts
|
|
195
|
+
}),
|
|
196
|
+
abort() {},
|
|
197
|
+
async commit() {},
|
|
198
|
+
dispose() {}
|
|
199
|
+
}), [engine, initialPageData]),
|
|
200
|
+
rollbackResource: null,
|
|
201
|
+
pendingId: null,
|
|
202
|
+
routeUrl: currentUrl(),
|
|
203
|
+
navigation: idleNavigationState()
|
|
204
|
+
});
|
|
205
|
+
const [isTransitionPending, startTransition] = useTransition();
|
|
206
|
+
const mergeShared = useCallback((result) => {
|
|
207
|
+
if (!result) return result;
|
|
208
|
+
sharedStateRef.current = {
|
|
209
|
+
...sharedStateRef.current,
|
|
210
|
+
...result.pageData.shared || {}
|
|
211
|
+
};
|
|
212
|
+
const shared = sharedStateRef.current;
|
|
213
|
+
result.pageData = {
|
|
214
|
+
...result.pageData,
|
|
215
|
+
shared
|
|
216
|
+
};
|
|
217
|
+
return result;
|
|
218
|
+
}, []);
|
|
219
|
+
const finishNavigation = useCallback((resource, result) => {
|
|
220
|
+
if (committedResources.has(resource.id)) return;
|
|
221
|
+
committedResources.add(resource.id);
|
|
222
|
+
resource.commit().then(() => {
|
|
223
|
+
const resolveViewTransition = viewTransitionWaiters.get(resource.id);
|
|
224
|
+
if (resolveViewTransition) {
|
|
225
|
+
viewTransitionWaiters.delete(resource.id);
|
|
226
|
+
resolveViewTransition();
|
|
227
|
+
}
|
|
228
|
+
const visitSettler = pendingVisitSettlers.get(resource.id);
|
|
229
|
+
if (visitSettler) {
|
|
230
|
+
pendingVisitSettlers.delete(resource.id);
|
|
231
|
+
visitSettler.resolve(result.pageData);
|
|
232
|
+
}
|
|
233
|
+
if (activePendingNavigationId === resource.id) activePendingNavigationId = null;
|
|
234
|
+
dispatch({
|
|
235
|
+
type: "finish",
|
|
236
|
+
resource,
|
|
237
|
+
location: currentUrl()
|
|
238
|
+
});
|
|
239
|
+
}).catch((error) => {
|
|
240
|
+
const visitSettler = pendingVisitSettlers.get(resource.id);
|
|
241
|
+
if (visitSettler) {
|
|
242
|
+
pendingVisitSettlers.delete(resource.id);
|
|
243
|
+
visitSettler.reject(error);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}, []);
|
|
247
|
+
const schedule = useCallback((url, options = {}) => {
|
|
248
|
+
const prepared = engine.prepareVisit(url, options);
|
|
249
|
+
if (prepared.immediate) {
|
|
250
|
+
const navigation = navigationForResource(prepared);
|
|
251
|
+
activePendingNavigationId = prepared.id;
|
|
252
|
+
dispatch({
|
|
253
|
+
type: "begin",
|
|
254
|
+
resource: prepared,
|
|
255
|
+
navigation
|
|
256
|
+
});
|
|
257
|
+
return prepared.shell.then((result) => {
|
|
258
|
+
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
259
|
+
dispatch({
|
|
260
|
+
type: "finish",
|
|
261
|
+
resource: prepared,
|
|
262
|
+
location: currentUrl()
|
|
263
|
+
});
|
|
264
|
+
return result?.pageData;
|
|
265
|
+
}, (error) => {
|
|
266
|
+
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
267
|
+
dispatch({
|
|
268
|
+
type: "fail",
|
|
269
|
+
resource: prepared,
|
|
270
|
+
location: currentUrl()
|
|
271
|
+
});
|
|
272
|
+
throw error;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
let resolveVisit;
|
|
276
|
+
let rejectVisit;
|
|
277
|
+
const pagePromise = new Promise((resolve, reject) => {
|
|
278
|
+
resolveVisit = resolve;
|
|
279
|
+
rejectVisit = reject;
|
|
280
|
+
});
|
|
281
|
+
const next = {
|
|
282
|
+
...prepared,
|
|
283
|
+
shell: prepared.shell.then(mergeShared).then((result) => {
|
|
284
|
+
if (!result) {
|
|
285
|
+
const resolveViewTransition = viewTransitionWaiters.get(prepared.id);
|
|
286
|
+
if (resolveViewTransition) {
|
|
287
|
+
viewTransitionWaiters.delete(prepared.id);
|
|
288
|
+
resolveViewTransition();
|
|
289
|
+
}
|
|
290
|
+
pendingVisitSettlers.delete(prepared.id);
|
|
291
|
+
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
292
|
+
dispatch({
|
|
293
|
+
type: "finish",
|
|
294
|
+
resource: prepared,
|
|
295
|
+
location: currentUrl()
|
|
296
|
+
});
|
|
297
|
+
resolveVisit(void 0);
|
|
298
|
+
} else if (prepared.options._resolveOnShell) {
|
|
299
|
+
pendingVisitSettlers.delete(prepared.id);
|
|
300
|
+
resolveVisit(result.pageData);
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}, (error) => {
|
|
304
|
+
const resolveViewTransition = viewTransitionWaiters.get(prepared.id);
|
|
305
|
+
if (resolveViewTransition) {
|
|
306
|
+
viewTransitionWaiters.delete(prepared.id);
|
|
307
|
+
resolveViewTransition();
|
|
308
|
+
}
|
|
309
|
+
pendingVisitSettlers.delete(prepared.id);
|
|
310
|
+
rejectVisit(error);
|
|
311
|
+
if (activePendingNavigationId === prepared.id) activePendingNavigationId = null;
|
|
312
|
+
if (prepared.options._resolveOnShell || isAbortError(error) || isCallSiteActionError(error)) {
|
|
313
|
+
dispatch({
|
|
314
|
+
type: "abandon",
|
|
315
|
+
resource: prepared,
|
|
316
|
+
location: currentUrl()
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
dispatch({
|
|
321
|
+
type: "fail",
|
|
322
|
+
resource: prepared,
|
|
323
|
+
location: currentUrl()
|
|
324
|
+
});
|
|
325
|
+
throw error;
|
|
326
|
+
})
|
|
327
|
+
};
|
|
328
|
+
const navigation = navigationForResource(next);
|
|
329
|
+
pendingVisitSettlers.set(next.id, {
|
|
330
|
+
resolve: resolveVisit,
|
|
331
|
+
reject: rejectVisit
|
|
332
|
+
});
|
|
333
|
+
activePendingNavigationId = next.id;
|
|
334
|
+
dispatch({
|
|
335
|
+
type: "begin",
|
|
336
|
+
resource: next,
|
|
337
|
+
navigation
|
|
338
|
+
});
|
|
339
|
+
const reveal = () => {
|
|
340
|
+
startTransition(() => {
|
|
341
|
+
dispatch({
|
|
342
|
+
type: "reveal",
|
|
343
|
+
resource: next
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
startViewTransition(next, reveal, options.viewTransition ?? viewTransitions);
|
|
348
|
+
return pagePromise;
|
|
349
|
+
}, [
|
|
350
|
+
engine,
|
|
351
|
+
mergeShared,
|
|
352
|
+
startTransition,
|
|
353
|
+
viewTransitions
|
|
354
|
+
]);
|
|
355
|
+
useInsertionEffect(() => {
|
|
356
|
+
scheduleVisit = schedule;
|
|
357
|
+
return () => {
|
|
358
|
+
if (scheduleVisit === schedule) scheduleVisit = null;
|
|
359
|
+
};
|
|
360
|
+
}, [schedule]);
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
engine.setPopStateHandler((url, options) => {
|
|
363
|
+
router.visit(url, options);
|
|
364
|
+
});
|
|
365
|
+
}, [engine, router]);
|
|
366
|
+
const navigationValue = useMemo(() => {
|
|
367
|
+
if (state.navigation.state !== "idle" || !isTransitionPending) return state.navigation;
|
|
368
|
+
return {
|
|
369
|
+
state: "loading",
|
|
370
|
+
location: null,
|
|
371
|
+
method: "GET"
|
|
372
|
+
};
|
|
373
|
+
}, [isTransitionPending, state.navigation]);
|
|
374
|
+
const routerValue = useMemo(() => createRouterFacade(router, () => state.routeUrl), [router, state.routeUrl]);
|
|
375
|
+
return React.createElement(RouterContext.Provider, { value: routerValue }, React.createElement(NavigationContext.Provider, { value: navigationValue }, React.createElement(Suspense, { fallback: null }, React.createElement(RouteResource, {
|
|
376
|
+
resource: state.resource,
|
|
377
|
+
onCommit: finishNavigation
|
|
378
|
+
}))));
|
|
379
|
+
}
|
|
380
|
+
async function startReactPages(config) {
|
|
381
|
+
viewTransitionWaiters.clear();
|
|
382
|
+
pendingVisitSettlers.clear();
|
|
383
|
+
committedResources.clear();
|
|
384
|
+
committedResources.add(0);
|
|
385
|
+
activePendingNavigationId = null;
|
|
386
|
+
const engine = await createPageNavigationEngine({
|
|
387
|
+
adapter: {
|
|
388
|
+
createDeferred: createDeferredResource,
|
|
389
|
+
resolveDeferred: resolveDeferredResource,
|
|
390
|
+
rejectDeferred: rejectDeferredResource,
|
|
391
|
+
isLoading: isDeferredResourceLoading
|
|
392
|
+
},
|
|
393
|
+
components: config.components,
|
|
394
|
+
layoutTree: config.layoutTree,
|
|
395
|
+
routeMeta: config.routeMeta,
|
|
396
|
+
matchRoute: config.matchRoute,
|
|
397
|
+
staticPageData: config.staticPageData,
|
|
398
|
+
staticPageDataFastPath: config.staticPageDataFastPath,
|
|
399
|
+
viewTransitions: config.viewTransitions,
|
|
400
|
+
prefetchHoverDelay: config.prefetchHoverDelay,
|
|
401
|
+
prefetchDefaultCacheFor: config.prefetchDefaultCacheFor
|
|
402
|
+
});
|
|
403
|
+
const router = createRouter(engine);
|
|
404
|
+
setActionRouter(router);
|
|
405
|
+
const appEl = document.getElementById("app");
|
|
406
|
+
if (!appEl) throw new Error("pages: Missing #app element for Void React pages hydration.");
|
|
407
|
+
hydrateRoot(appEl, React.createElement(App, {
|
|
408
|
+
engine,
|
|
409
|
+
router,
|
|
410
|
+
viewTransitions: config.viewTransitions
|
|
411
|
+
}));
|
|
412
|
+
appEl.setAttribute("data-hydrated", "true");
|
|
413
|
+
engine.start();
|
|
414
|
+
return { router };
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
417
|
+
export { startReactPages };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { PageObject } from "void/pages-protocol";
|
|
3
|
+
|
|
4
|
+
//#region src/runtime/pages-server.d.ts
|
|
5
|
+
type ComponentModule = {
|
|
6
|
+
default: ComponentType<any>;
|
|
7
|
+
};
|
|
8
|
+
interface ReactPagesServerConfig {
|
|
9
|
+
pageObj: PageObject;
|
|
10
|
+
assetTags: {
|
|
11
|
+
css: string;
|
|
12
|
+
preloads: string;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
components: Record<string, () => Promise<ComponentModule>>;
|
|
16
|
+
layoutTree: Record<string, Array<string>>;
|
|
17
|
+
preambleScript?: string;
|
|
18
|
+
}
|
|
19
|
+
declare function renderReactPage({
|
|
20
|
+
pageObj,
|
|
21
|
+
assetTags,
|
|
22
|
+
components,
|
|
23
|
+
layoutTree,
|
|
24
|
+
preambleScript
|
|
25
|
+
}: ReactPagesServerConfig): Promise<ReadableStream<Uint8Array>>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { ReactPagesServerConfig, renderReactPage };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { i as SharedContext, n as NavigationContext, r as RouterContext, t as ErrorsContext } from "../context-BCeFV8Jy.mjs";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createSsrRouter, idleNavigationState } from "void/pages-client";
|
|
4
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
5
|
+
import { renderBodyAttrs, renderHeadToString, renderHtmlAttrs } from "void/pages-head";
|
|
6
|
+
import { serializePageData } from "void/pages-server";
|
|
7
|
+
//#region src/runtime/pages-server.ts
|
|
8
|
+
const DEFERRED_SHELL_CANCEL_REASON = "Void deferred shell complete";
|
|
9
|
+
const DEFERRED_SHELL_END_ID = "__VOID_DEFERRED_SHELL_END__";
|
|
10
|
+
const DEFERRED_SHELL_END_MARKER = `<template id="${DEFERRED_SHELL_END_ID}"></template>`;
|
|
11
|
+
function createPendingDeferred() {
|
|
12
|
+
return new Promise(() => {});
|
|
13
|
+
}
|
|
14
|
+
function propsForReactRender(pageObj) {
|
|
15
|
+
if (!pageObj.deferredKeys) return pageObj.props;
|
|
16
|
+
const props = { ...pageObj.props };
|
|
17
|
+
for (const key of Object.keys(pageObj.deferredKeys)) props[key] = createPendingDeferred();
|
|
18
|
+
return props;
|
|
19
|
+
}
|
|
20
|
+
function textStream(text) {
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
return new ReadableStream({ start(controller) {
|
|
23
|
+
controller.enqueue(encoder.encode(text));
|
|
24
|
+
controller.close();
|
|
25
|
+
} });
|
|
26
|
+
}
|
|
27
|
+
function concatStreams(streams) {
|
|
28
|
+
return new ReadableStream({ async start(controller) {
|
|
29
|
+
try {
|
|
30
|
+
for (const stream of streams) {
|
|
31
|
+
const reader = stream.getReader();
|
|
32
|
+
while (true) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done) break;
|
|
35
|
+
controller.enqueue(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
controller.close();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
controller.error(error);
|
|
41
|
+
}
|
|
42
|
+
} });
|
|
43
|
+
}
|
|
44
|
+
function readDeferredShell(stream) {
|
|
45
|
+
const encoder = new TextEncoder();
|
|
46
|
+
return new ReadableStream({ async start(controller) {
|
|
47
|
+
const reader = stream.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
let buffer = "";
|
|
50
|
+
const keepLength = DEFERRED_SHELL_END_MARKER.length - 1;
|
|
51
|
+
const flush = (text) => {
|
|
52
|
+
if (text) controller.enqueue(encoder.encode(text));
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
while (true) {
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (done) {
|
|
58
|
+
buffer += decoder.decode();
|
|
59
|
+
flush(buffer);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
buffer += decoder.decode(value, { stream: true });
|
|
63
|
+
const markerIndex = buffer.indexOf(DEFERRED_SHELL_END_MARKER);
|
|
64
|
+
if (markerIndex !== -1) {
|
|
65
|
+
flush(buffer.slice(0, markerIndex));
|
|
66
|
+
await reader.cancel(DEFERRED_SHELL_CANCEL_REASON);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (buffer.length > keepLength) {
|
|
70
|
+
flush(buffer.slice(0, -keepLength));
|
|
71
|
+
buffer = buffer.slice(-keepLength);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
controller.close();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
controller.error(error);
|
|
77
|
+
}
|
|
78
|
+
} });
|
|
79
|
+
}
|
|
80
|
+
async function renderReactPage({ pageObj, assetTags, components, layoutTree, preambleScript = "" }) {
|
|
81
|
+
const PageComponent = (await components[pageObj.component]()).default;
|
|
82
|
+
const layoutIds = layoutTree[pageObj.component] || [];
|
|
83
|
+
const layoutComponents = await Promise.all(layoutIds.map(async (id) => (await components[id]()).default));
|
|
84
|
+
const renderProps = propsForReactRender(pageObj);
|
|
85
|
+
let element = React.createElement(PageComponent, renderProps);
|
|
86
|
+
for (let i = layoutComponents.length - 1; i >= 0; i--) element = React.createElement(layoutComponents[i], null, element);
|
|
87
|
+
element = React.createElement(RouterContext.Provider, { value: createSsrRouter(pageObj.url || "/") }, React.createElement(NavigationContext.Provider, { value: idleNavigationState() }, React.createElement(React.Suspense, { fallback: null }, React.createElement(SharedContext.Provider, { value: pageObj.shared || {} }, React.createElement(ErrorsContext.Provider, { value: pageObj.errors || {} }, element)))));
|
|
88
|
+
const appStream = await renderToReadableStream(pageObj.deferredKeys ? React.createElement(React.Fragment, null, element, React.createElement("template", { id: DEFERRED_SHELL_END_ID })) : element, { onError(error) {
|
|
89
|
+
if (error === DEFERRED_SHELL_CANCEL_REASON) return;
|
|
90
|
+
throw error;
|
|
91
|
+
} });
|
|
92
|
+
const renderedAppStream = pageObj.deferredKeys ? readDeferredShell(appStream) : appStream;
|
|
93
|
+
const pageData = serializePageData(pageObj);
|
|
94
|
+
const headHtml = pageObj.head ? renderHeadToString(pageObj.head) : "";
|
|
95
|
+
const htmlAttrs = pageObj.head ? renderHtmlAttrs(pageObj.head) : "";
|
|
96
|
+
const bodyAttrs = pageObj.head ? renderBodyAttrs(pageObj.head) : "";
|
|
97
|
+
return concatStreams([
|
|
98
|
+
textStream(`<!doctype html>
|
|
99
|
+
<html${htmlAttrs}>
|
|
100
|
+
<head>${headHtml}${preambleScript}${assetTags.css}${assetTags.preloads}</head>
|
|
101
|
+
<body${bodyAttrs}>
|
|
102
|
+
<script id="__VOID_PAGE_DATA__" type="application/json">${pageData}<\/script>
|
|
103
|
+
<div id="app">`),
|
|
104
|
+
renderedAppStream,
|
|
105
|
+
textStream(`</div>
|
|
106
|
+
${assetTags.body}
|
|
107
|
+
</body>
|
|
108
|
+
</html>`)
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { renderReactPage };
|