@alepha/react 0.9.3 → 0.9.5
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 +64 -6
- package/dist/index.browser.js +442 -328
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +644 -482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -339
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +412 -349
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +641 -484
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +164 -19
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +100 -5
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -35
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -4
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -31
- package/src/hooks/useRouterEvents.ts +30 -22
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +0 -7
- package/src/index.browser.ts +14 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +27 -31
- package/src/providers/ReactBrowserProvider.ts +151 -62
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +137 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +121 -104
- package/src/providers/ReactServerProvider.ts +90 -76
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +49 -62
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- package/src/providers/ReactBrowserRenderer.ts +0 -93
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { $env, $hook, $inject, $
|
|
2
|
-
import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider
|
|
1
|
+
import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, TypeGuard, createDescriptor, t } from "@alepha/core";
|
|
2
|
+
import { AlephaServer, HttpClient, ServerProvider, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { $logger } from "@alepha/logger";
|
|
6
|
+
import React, { StrictMode, createContext, createElement, memo, use, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
11
11
|
import { renderToString } from "react-dom/server";
|
|
12
|
+
import { DateTimeProvider } from "@alepha/datetime";
|
|
13
|
+
import { RouterProvider } from "@alepha/router";
|
|
12
14
|
|
|
13
15
|
//#region src/descriptors/$page.ts
|
|
14
16
|
/**
|
|
@@ -32,7 +34,16 @@ var PageDescriptor = class extends Descriptor {
|
|
|
32
34
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
33
35
|
*/
|
|
34
36
|
async render(options) {
|
|
35
|
-
throw new
|
|
37
|
+
throw new AlephaError("render() method is not implemented in this environment");
|
|
38
|
+
}
|
|
39
|
+
async fetch(options) {
|
|
40
|
+
throw new AlephaError("fetch() method is not implemented in this environment");
|
|
41
|
+
}
|
|
42
|
+
match(url) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
pathname(config) {
|
|
46
|
+
return this.options.path || "";
|
|
36
47
|
}
|
|
37
48
|
};
|
|
38
49
|
$page[KIND] = PageDescriptor;
|
|
@@ -55,7 +66,6 @@ const ClientOnly = (props) => {
|
|
|
55
66
|
if (props.disabled) return props.children;
|
|
56
67
|
return mounted ? props.children : props.fallback;
|
|
57
68
|
};
|
|
58
|
-
var ClientOnly_default = ClientOnly;
|
|
59
69
|
|
|
60
70
|
//#endregion
|
|
61
71
|
//#region src/components/ErrorViewer.tsx
|
|
@@ -163,7 +173,6 @@ const ErrorViewer = ({ error, alepha }) => {
|
|
|
163
173
|
})] })]
|
|
164
174
|
});
|
|
165
175
|
};
|
|
166
|
-
var ErrorViewer_default = ErrorViewer;
|
|
167
176
|
const ErrorViewerProduction = () => {
|
|
168
177
|
const styles = {
|
|
169
178
|
container: {
|
|
@@ -205,45 +214,107 @@ const ErrorViewerProduction = () => {
|
|
|
205
214
|
});
|
|
206
215
|
};
|
|
207
216
|
|
|
208
|
-
//#endregion
|
|
209
|
-
//#region src/contexts/RouterContext.ts
|
|
210
|
-
const RouterContext = createContext(void 0);
|
|
211
|
-
|
|
212
217
|
//#endregion
|
|
213
218
|
//#region src/contexts/RouterLayerContext.ts
|
|
214
219
|
const RouterLayerContext = createContext(void 0);
|
|
215
220
|
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/errors/Redirection.ts
|
|
223
|
+
/**
|
|
224
|
+
* Used for Redirection during the page loading.
|
|
225
|
+
*
|
|
226
|
+
* Depends on the context, it can be thrown or just returned.
|
|
227
|
+
*/
|
|
228
|
+
var Redirection = class extends Error {
|
|
229
|
+
redirect;
|
|
230
|
+
constructor(redirect) {
|
|
231
|
+
super("Redirection");
|
|
232
|
+
this.redirect = redirect;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
216
236
|
//#endregion
|
|
217
237
|
//#region src/contexts/AlephaContext.ts
|
|
218
238
|
const AlephaContext = createContext(void 0);
|
|
219
239
|
|
|
220
240
|
//#endregion
|
|
221
241
|
//#region src/hooks/useAlepha.ts
|
|
242
|
+
/**
|
|
243
|
+
* Main Alepha hook.
|
|
244
|
+
*
|
|
245
|
+
* It provides access to the Alepha instance within a React component.
|
|
246
|
+
*
|
|
247
|
+
* With Alepha, you can access the core functionalities of the framework:
|
|
248
|
+
*
|
|
249
|
+
* - alepha.state() for state management
|
|
250
|
+
* - alepha.inject() for dependency injection
|
|
251
|
+
* - alepha.emit() for event handling
|
|
252
|
+
* etc...
|
|
253
|
+
*/
|
|
222
254
|
const useAlepha = () => {
|
|
223
255
|
const alepha = useContext(AlephaContext);
|
|
224
|
-
if (!alepha) throw new
|
|
256
|
+
if (!alepha) throw new AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
|
|
225
257
|
return alepha;
|
|
226
258
|
};
|
|
227
259
|
|
|
228
260
|
//#endregion
|
|
229
261
|
//#region src/hooks/useRouterEvents.ts
|
|
262
|
+
/**
|
|
263
|
+
* Subscribe to various router events.
|
|
264
|
+
*/
|
|
230
265
|
const useRouterEvents = (opts = {}, deps = []) => {
|
|
231
266
|
const alepha = useAlepha();
|
|
232
267
|
useEffect(() => {
|
|
233
268
|
if (!alepha.isBrowser()) return;
|
|
269
|
+
const cb = (callback) => {
|
|
270
|
+
if (typeof callback === "function") return { callback };
|
|
271
|
+
return callback;
|
|
272
|
+
};
|
|
234
273
|
const subs = [];
|
|
235
274
|
const onBegin = opts.onBegin;
|
|
236
275
|
const onEnd = opts.onEnd;
|
|
237
276
|
const onError = opts.onError;
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
if (
|
|
277
|
+
const onSuccess = opts.onSuccess;
|
|
278
|
+
if (onBegin) subs.push(alepha.on("react:transition:begin", cb(onBegin)));
|
|
279
|
+
if (onEnd) subs.push(alepha.on("react:transition:end", cb(onEnd)));
|
|
280
|
+
if (onError) subs.push(alepha.on("react:transition:error", cb(onError)));
|
|
281
|
+
if (onSuccess) subs.push(alepha.on("react:transition:success", cb(onSuccess)));
|
|
241
282
|
return () => {
|
|
242
283
|
for (const sub of subs) sub();
|
|
243
284
|
};
|
|
244
285
|
}, deps);
|
|
245
286
|
};
|
|
246
287
|
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/hooks/useStore.ts
|
|
290
|
+
/**
|
|
291
|
+
* Hook to access and mutate the Alepha state.
|
|
292
|
+
*/
|
|
293
|
+
const useStore = (key, defaultValue) => {
|
|
294
|
+
const alepha = useAlepha();
|
|
295
|
+
useMemo(() => {
|
|
296
|
+
if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
|
|
297
|
+
}, [defaultValue]);
|
|
298
|
+
const [state, setState] = useState(alepha.state(key));
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!alepha.isBrowser()) return;
|
|
301
|
+
return alepha.on("state:mutate", (ev) => {
|
|
302
|
+
if (ev.key === key) setState(ev.value);
|
|
303
|
+
});
|
|
304
|
+
}, []);
|
|
305
|
+
return [state, (value) => {
|
|
306
|
+
alepha.state(key, value);
|
|
307
|
+
}];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/hooks/useRouterState.ts
|
|
312
|
+
const useRouterState = () => {
|
|
313
|
+
const [state] = useStore("react.router.state");
|
|
314
|
+
if (!state) throw new AlephaError("Missing react router state");
|
|
315
|
+
return state;
|
|
316
|
+
};
|
|
317
|
+
|
|
247
318
|
//#endregion
|
|
248
319
|
//#region src/components/ErrorBoundary.tsx
|
|
249
320
|
/**
|
|
@@ -273,7 +344,6 @@ var ErrorBoundary = class extends React.Component {
|
|
|
273
344
|
return this.props.children;
|
|
274
345
|
}
|
|
275
346
|
};
|
|
276
|
-
var ErrorBoundary_default = ErrorBoundary;
|
|
277
347
|
|
|
278
348
|
//#endregion
|
|
279
349
|
//#region src/components/NestedView.tsx
|
|
@@ -284,7 +354,7 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
284
354
|
*
|
|
285
355
|
* @example
|
|
286
356
|
* ```tsx
|
|
287
|
-
* import { NestedView } from "
|
|
357
|
+
* import { NestedView } from "alepha/react";
|
|
288
358
|
*
|
|
289
359
|
* class App {
|
|
290
360
|
* parent = $page({
|
|
@@ -299,24 +369,108 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
299
369
|
* ```
|
|
300
370
|
*/
|
|
301
371
|
const NestedView = (props) => {
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const [
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
372
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
373
|
+
const state = useRouterState();
|
|
374
|
+
const [view, setView] = useState(state.layers[index]?.element);
|
|
375
|
+
const [animation, setAnimation] = useState("");
|
|
376
|
+
const animationExitDuration = useRef(0);
|
|
377
|
+
const animationExitNow = useRef(0);
|
|
378
|
+
useRouterEvents({
|
|
379
|
+
onBegin: async ({ previous, state: state$1 }) => {
|
|
380
|
+
const layer = previous.layers[index];
|
|
381
|
+
if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
|
|
382
|
+
const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
|
|
383
|
+
if (animationExit) {
|
|
384
|
+
const duration = animationExit.duration || 200;
|
|
385
|
+
animationExitNow.current = Date.now();
|
|
386
|
+
animationExitDuration.current = duration;
|
|
387
|
+
setAnimation(animationExit.animation);
|
|
388
|
+
} else {
|
|
389
|
+
animationExitNow.current = 0;
|
|
390
|
+
animationExitDuration.current = 0;
|
|
391
|
+
setAnimation("");
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
onEnd: async ({ state: state$1 }) => {
|
|
395
|
+
const layer = state$1.layers[index];
|
|
396
|
+
if (animationExitNow.current) {
|
|
397
|
+
const duration = animationExitDuration.current;
|
|
398
|
+
const diff = Date.now() - animationExitNow.current;
|
|
399
|
+
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
|
|
400
|
+
}
|
|
401
|
+
if (!layer?.cache) {
|
|
402
|
+
setView(layer?.element);
|
|
403
|
+
const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
|
|
404
|
+
if (animationEnter) setAnimation(animationEnter.animation);
|
|
405
|
+
else setAnimation("");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}, []);
|
|
409
|
+
let element = view ?? props.children ?? null;
|
|
410
|
+
if (animation) element = /* @__PURE__ */ jsx("div", {
|
|
411
|
+
style: {
|
|
412
|
+
display: "flex",
|
|
413
|
+
flex: 1,
|
|
414
|
+
height: "100%",
|
|
415
|
+
width: "100%",
|
|
416
|
+
position: "relative",
|
|
417
|
+
overflow: "hidden"
|
|
418
|
+
},
|
|
419
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
420
|
+
style: {
|
|
421
|
+
height: "100%",
|
|
422
|
+
width: "100%",
|
|
423
|
+
display: "flex",
|
|
424
|
+
animation
|
|
425
|
+
},
|
|
426
|
+
children: element
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
|
|
430
|
+
if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
431
|
+
fallback: props.errorBoundary,
|
|
432
|
+
children: element
|
|
433
|
+
});
|
|
434
|
+
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
313
435
|
fallback: (error) => {
|
|
314
|
-
|
|
436
|
+
const result = state.onError(error, state);
|
|
437
|
+
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
438
|
+
return result;
|
|
315
439
|
},
|
|
316
440
|
children: element
|
|
317
441
|
});
|
|
318
442
|
};
|
|
319
|
-
var NestedView_default = NestedView;
|
|
443
|
+
var NestedView_default = memo(NestedView);
|
|
444
|
+
function parseAnimation(animationLike, state, type = "enter") {
|
|
445
|
+
if (!animationLike) return void 0;
|
|
446
|
+
const DEFAULT_DURATION = 300;
|
|
447
|
+
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
448
|
+
if (typeof animation === "string") {
|
|
449
|
+
if (type === "exit") return;
|
|
450
|
+
return {
|
|
451
|
+
duration: DEFAULT_DURATION,
|
|
452
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (typeof animation === "object") {
|
|
456
|
+
const anim = animation[type];
|
|
457
|
+
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
|
|
458
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
459
|
+
if (type === "exit") {
|
|
460
|
+
const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
461
|
+
return {
|
|
462
|
+
duration,
|
|
463
|
+
animation: `${duration}ms ${timing$1} ${name}`
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const timing = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
467
|
+
return {
|
|
468
|
+
duration,
|
|
469
|
+
animation: `${duration}ms ${timing} ${name}`
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
320
474
|
|
|
321
475
|
//#endregion
|
|
322
476
|
//#region src/components/NotFound.tsx
|
|
@@ -338,27 +492,17 @@ function NotFoundPage(props) {
|
|
|
338
492
|
fontSize: "1rem",
|
|
339
493
|
marginBottom: "0.5rem"
|
|
340
494
|
},
|
|
341
|
-
children: "This page does not exist"
|
|
495
|
+
children: "404 - This page does not exist"
|
|
342
496
|
})
|
|
343
497
|
});
|
|
344
498
|
}
|
|
345
499
|
|
|
346
500
|
//#endregion
|
|
347
|
-
//#region src/
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
constructor(page) {
|
|
351
|
-
super("Redirection");
|
|
352
|
-
this.page = page;
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
//#endregion
|
|
357
|
-
//#region src/providers/PageDescriptorProvider.ts
|
|
358
|
-
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
359
|
-
var PageDescriptorProvider = class {
|
|
501
|
+
//#region src/providers/ReactPageProvider.ts
|
|
502
|
+
const envSchema$2 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
503
|
+
var ReactPageProvider = class {
|
|
360
504
|
log = $logger();
|
|
361
|
-
env = $env(envSchema$
|
|
505
|
+
env = $env(envSchema$2);
|
|
362
506
|
alepha = $inject(Alepha);
|
|
363
507
|
pages = [];
|
|
364
508
|
getPages() {
|
|
@@ -385,22 +529,29 @@ var PageDescriptorProvider = class {
|
|
|
385
529
|
return url.replace(/\/\/+/g, "/") || "/";
|
|
386
530
|
}
|
|
387
531
|
url(name, options = {}) {
|
|
388
|
-
return new URL(this.pathname(name, options), options.
|
|
532
|
+
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
|
|
389
533
|
}
|
|
390
|
-
root(state
|
|
391
|
-
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(
|
|
392
|
-
state,
|
|
393
|
-
context
|
|
394
|
-
} }, createElement(NestedView_default, {}, state.layers[0]?.element)));
|
|
534
|
+
root(state) {
|
|
535
|
+
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
|
|
395
536
|
if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
|
|
396
537
|
return root;
|
|
397
538
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
539
|
+
convertStringObjectToObject = (schema, value) => {
|
|
540
|
+
if (TypeGuard.IsObject(schema) && typeof value === "object") {
|
|
541
|
+
for (const key in schema.properties) if (TypeGuard.IsObject(schema.properties[key]) && typeof value[key] === "string") try {
|
|
542
|
+
value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
|
|
543
|
+
} catch (e) {}
|
|
544
|
+
}
|
|
545
|
+
return value;
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* Create a new RouterState based on a given route and request.
|
|
549
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
550
|
+
* It also handles errors and redirects.
|
|
551
|
+
*/
|
|
552
|
+
async createLayers(route, state, previous = []) {
|
|
401
553
|
let context = {};
|
|
402
554
|
const stack = [{ route }];
|
|
403
|
-
request.onError = (error) => this.renderError(error);
|
|
404
555
|
let parent = route.parent;
|
|
405
556
|
while (parent) {
|
|
406
557
|
stack.unshift({ route: parent });
|
|
@@ -412,19 +563,19 @@ var PageDescriptorProvider = class {
|
|
|
412
563
|
const route$1 = it.route;
|
|
413
564
|
const config = {};
|
|
414
565
|
try {
|
|
415
|
-
|
|
566
|
+
this.convertStringObjectToObject(route$1.schema?.query, state.query);
|
|
567
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
416
568
|
} catch (e) {
|
|
417
569
|
it.error = e;
|
|
418
570
|
break;
|
|
419
571
|
}
|
|
420
572
|
try {
|
|
421
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params,
|
|
573
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
|
|
422
574
|
} catch (e) {
|
|
423
575
|
it.error = e;
|
|
424
576
|
break;
|
|
425
577
|
}
|
|
426
578
|
it.config = { ...config };
|
|
427
|
-
const previous = request.previous;
|
|
428
579
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
429
580
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
430
581
|
const prev = JSON.stringify({
|
|
@@ -450,7 +601,7 @@ var PageDescriptorProvider = class {
|
|
|
450
601
|
if (!route$1.resolve) continue;
|
|
451
602
|
try {
|
|
452
603
|
const props = await route$1.resolve?.({
|
|
453
|
-
...
|
|
604
|
+
...state,
|
|
454
605
|
...config,
|
|
455
606
|
...context
|
|
456
607
|
}) ?? {};
|
|
@@ -460,11 +611,8 @@ var PageDescriptorProvider = class {
|
|
|
460
611
|
...props
|
|
461
612
|
};
|
|
462
613
|
} catch (e) {
|
|
463
|
-
if (e instanceof Redirection) return
|
|
464
|
-
|
|
465
|
-
search
|
|
466
|
-
});
|
|
467
|
-
this.log.error(e);
|
|
614
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
615
|
+
this.log.error("Page resolver has failed", e);
|
|
468
616
|
it.error = e;
|
|
469
617
|
break;
|
|
470
618
|
}
|
|
@@ -480,69 +628,58 @@ var PageDescriptorProvider = class {
|
|
|
480
628
|
const path = acc.replace(/\/+/, "/");
|
|
481
629
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
482
630
|
if (localErrorHandler) {
|
|
483
|
-
const onErrorParent =
|
|
484
|
-
|
|
631
|
+
const onErrorParent = state.onError;
|
|
632
|
+
state.onError = (error, context$1) => {
|
|
485
633
|
const result = localErrorHandler(error, context$1);
|
|
486
634
|
if (result === void 0) return onErrorParent(error, context$1);
|
|
487
635
|
return result;
|
|
488
636
|
};
|
|
489
637
|
}
|
|
490
|
-
if (it.error) try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
pathname,
|
|
495
|
-
search
|
|
638
|
+
if (!it.error) try {
|
|
639
|
+
const element = await this.createElement(it.route, {
|
|
640
|
+
...props,
|
|
641
|
+
...context
|
|
496
642
|
});
|
|
497
|
-
|
|
498
|
-
|
|
643
|
+
state.layers.push({
|
|
644
|
+
name: it.route.name,
|
|
645
|
+
props,
|
|
646
|
+
part: it.route.path,
|
|
647
|
+
config: it.config,
|
|
648
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
649
|
+
index: i + 1,
|
|
650
|
+
path,
|
|
651
|
+
route: it.route,
|
|
652
|
+
cache: it.cache
|
|
653
|
+
});
|
|
654
|
+
} catch (e) {
|
|
655
|
+
it.error = e;
|
|
656
|
+
}
|
|
657
|
+
if (it.error) try {
|
|
658
|
+
let element = await state.onError(it.error, state);
|
|
659
|
+
if (element === void 0) throw it.error;
|
|
660
|
+
if (element instanceof Redirection) return { redirect: element.redirect };
|
|
661
|
+
if (element === null) element = this.renderError(it.error);
|
|
662
|
+
state.layers.push({
|
|
499
663
|
props,
|
|
500
664
|
error: it.error,
|
|
501
665
|
name: it.route.name,
|
|
502
666
|
part: it.route.path,
|
|
503
667
|
config: it.config,
|
|
504
|
-
element: this.renderView(i + 1, path, element
|
|
668
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
505
669
|
index: i + 1,
|
|
506
670
|
path,
|
|
507
671
|
route: it.route
|
|
508
672
|
});
|
|
509
673
|
break;
|
|
510
674
|
} catch (e) {
|
|
511
|
-
if (e instanceof Redirection) return
|
|
512
|
-
pathname,
|
|
513
|
-
search
|
|
514
|
-
});
|
|
675
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
515
676
|
throw e;
|
|
516
677
|
}
|
|
517
|
-
const element = await this.createElement(it.route, {
|
|
518
|
-
...props,
|
|
519
|
-
...context
|
|
520
|
-
});
|
|
521
|
-
layers.push({
|
|
522
|
-
name: it.route.name,
|
|
523
|
-
props,
|
|
524
|
-
part: it.route.path,
|
|
525
|
-
config: it.config,
|
|
526
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
527
|
-
index: i + 1,
|
|
528
|
-
path,
|
|
529
|
-
route: it.route,
|
|
530
|
-
cache: it.cache
|
|
531
|
-
});
|
|
532
678
|
}
|
|
533
|
-
return {
|
|
534
|
-
layers,
|
|
535
|
-
pathname,
|
|
536
|
-
search
|
|
537
|
-
};
|
|
679
|
+
return { state };
|
|
538
680
|
}
|
|
539
|
-
createRedirectionLayer(
|
|
540
|
-
return {
|
|
541
|
-
layers: [],
|
|
542
|
-
redirect: typeof href === "string" ? href : this.href(href),
|
|
543
|
-
pathname: context.pathname,
|
|
544
|
-
search: context.search
|
|
545
|
-
};
|
|
681
|
+
createRedirectionLayer(redirect) {
|
|
682
|
+
return { redirect };
|
|
546
683
|
}
|
|
547
684
|
getErrorHandler(route) {
|
|
548
685
|
if (route.errorHandler) return route.errorHandler;
|
|
@@ -553,6 +690,7 @@ var PageDescriptorProvider = class {
|
|
|
553
690
|
}
|
|
554
691
|
}
|
|
555
692
|
async createElement(page, props) {
|
|
693
|
+
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
|
|
556
694
|
if (page.lazy) {
|
|
557
695
|
const component = await page.lazy();
|
|
558
696
|
return createElement(component.default, props);
|
|
@@ -561,7 +699,7 @@ var PageDescriptorProvider = class {
|
|
|
561
699
|
return void 0;
|
|
562
700
|
}
|
|
563
701
|
renderError(error) {
|
|
564
|
-
return createElement(
|
|
702
|
+
return createElement(ErrorViewer, {
|
|
565
703
|
error,
|
|
566
704
|
alepha: this.alepha
|
|
567
705
|
});
|
|
@@ -587,7 +725,7 @@ var PageDescriptorProvider = class {
|
|
|
587
725
|
}
|
|
588
726
|
renderView(index, path, view, page) {
|
|
589
727
|
view ??= this.renderEmptyView();
|
|
590
|
-
const element = page.client ? createElement(
|
|
728
|
+
const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
591
729
|
return createElement(RouterLayerContext.Provider, { value: {
|
|
592
730
|
index,
|
|
593
731
|
path
|
|
@@ -668,223 +806,9 @@ const isPageRoute = (it) => {
|
|
|
668
806
|
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
669
807
|
};
|
|
670
808
|
|
|
671
|
-
//#endregion
|
|
672
|
-
//#region src/providers/BrowserRouterProvider.ts
|
|
673
|
-
var BrowserRouterProvider = class extends RouterProvider {
|
|
674
|
-
log = $logger();
|
|
675
|
-
alepha = $inject(Alepha);
|
|
676
|
-
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
677
|
-
add(entry) {
|
|
678
|
-
this.pageDescriptorProvider.add(entry);
|
|
679
|
-
}
|
|
680
|
-
configure = $hook({
|
|
681
|
-
on: "configure",
|
|
682
|
-
handler: async () => {
|
|
683
|
-
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
684
|
-
path: page.match,
|
|
685
|
-
page
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
async transition(url, options = {}) {
|
|
690
|
-
const { pathname, search } = url;
|
|
691
|
-
const state = {
|
|
692
|
-
pathname,
|
|
693
|
-
search,
|
|
694
|
-
layers: []
|
|
695
|
-
};
|
|
696
|
-
const context = {
|
|
697
|
-
url,
|
|
698
|
-
query: {},
|
|
699
|
-
params: {},
|
|
700
|
-
onError: () => null,
|
|
701
|
-
...options.context ?? {}
|
|
702
|
-
};
|
|
703
|
-
await this.alepha.emit("react:transition:begin", {
|
|
704
|
-
state,
|
|
705
|
-
context
|
|
706
|
-
});
|
|
707
|
-
try {
|
|
708
|
-
const previous = options.previous;
|
|
709
|
-
const { route, params } = this.match(pathname);
|
|
710
|
-
const query = {};
|
|
711
|
-
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
712
|
-
context.query = query;
|
|
713
|
-
context.params = params ?? {};
|
|
714
|
-
context.previous = previous;
|
|
715
|
-
if (isPageRoute(route)) {
|
|
716
|
-
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
717
|
-
if (result.redirect) return {
|
|
718
|
-
redirect: result.redirect,
|
|
719
|
-
state,
|
|
720
|
-
context
|
|
721
|
-
};
|
|
722
|
-
state.layers = result.layers;
|
|
723
|
-
}
|
|
724
|
-
if (state.layers.length === 0) state.layers.push({
|
|
725
|
-
name: "not-found",
|
|
726
|
-
element: createElement(NotFoundPage),
|
|
727
|
-
index: 0,
|
|
728
|
-
path: "/"
|
|
729
|
-
});
|
|
730
|
-
await this.alepha.emit("react:transition:success", {
|
|
731
|
-
state,
|
|
732
|
-
context
|
|
733
|
-
});
|
|
734
|
-
} catch (e) {
|
|
735
|
-
this.log.error(e);
|
|
736
|
-
state.layers = [{
|
|
737
|
-
name: "error",
|
|
738
|
-
element: this.pageDescriptorProvider.renderError(e),
|
|
739
|
-
index: 0,
|
|
740
|
-
path: "/"
|
|
741
|
-
}];
|
|
742
|
-
await this.alepha.emit("react:transition:error", {
|
|
743
|
-
error: e,
|
|
744
|
-
state,
|
|
745
|
-
context
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (options.state) {
|
|
749
|
-
options.state.layers = state.layers;
|
|
750
|
-
options.state.pathname = state.pathname;
|
|
751
|
-
options.state.search = state.search;
|
|
752
|
-
}
|
|
753
|
-
if (options.previous) for (let i = 0; i < options.previous.length; i++) {
|
|
754
|
-
const layer = options.previous[i];
|
|
755
|
-
if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
|
|
756
|
-
}
|
|
757
|
-
await this.alepha.emit("react:transition:end", {
|
|
758
|
-
state: options.state,
|
|
759
|
-
context
|
|
760
|
-
});
|
|
761
|
-
return {
|
|
762
|
-
context,
|
|
763
|
-
state
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
root(state, context) {
|
|
767
|
-
return this.pageDescriptorProvider.root(state, context);
|
|
768
|
-
}
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region src/providers/ReactBrowserProvider.ts
|
|
773
|
-
var ReactBrowserProvider = class {
|
|
774
|
-
log = $logger();
|
|
775
|
-
client = $inject(LinkProvider);
|
|
776
|
-
alepha = $inject(Alepha);
|
|
777
|
-
router = $inject(BrowserRouterProvider);
|
|
778
|
-
root;
|
|
779
|
-
transitioning;
|
|
780
|
-
state = {
|
|
781
|
-
layers: [],
|
|
782
|
-
pathname: "",
|
|
783
|
-
search: ""
|
|
784
|
-
};
|
|
785
|
-
get document() {
|
|
786
|
-
return window.document;
|
|
787
|
-
}
|
|
788
|
-
get history() {
|
|
789
|
-
return window.history;
|
|
790
|
-
}
|
|
791
|
-
get location() {
|
|
792
|
-
return window.location;
|
|
793
|
-
}
|
|
794
|
-
get url() {
|
|
795
|
-
let url = this.location.pathname + this.location.search;
|
|
796
|
-
if (import.meta?.env?.BASE_URL) {
|
|
797
|
-
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
798
|
-
if (!url.startsWith("/")) url = `/${url}`;
|
|
799
|
-
}
|
|
800
|
-
return url;
|
|
801
|
-
}
|
|
802
|
-
pushState(url, replace) {
|
|
803
|
-
let path = url;
|
|
804
|
-
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
805
|
-
if (replace) this.history.replaceState({}, "", path);
|
|
806
|
-
else this.history.pushState({}, "", path);
|
|
807
|
-
}
|
|
808
|
-
async invalidate(props) {
|
|
809
|
-
const previous = [];
|
|
810
|
-
if (props) {
|
|
811
|
-
const [key] = Object.keys(props);
|
|
812
|
-
const value = props[key];
|
|
813
|
-
for (const layer of this.state.layers) {
|
|
814
|
-
if (layer.props?.[key]) {
|
|
815
|
-
previous.push({
|
|
816
|
-
...layer,
|
|
817
|
-
props: {
|
|
818
|
-
...layer.props,
|
|
819
|
-
[key]: value
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
break;
|
|
823
|
-
}
|
|
824
|
-
previous.push(layer);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
await this.render({ previous });
|
|
828
|
-
}
|
|
829
|
-
async go(url, options = {}) {
|
|
830
|
-
const result = await this.render({ url });
|
|
831
|
-
if (result.context.url.pathname + result.context.url.search !== url) {
|
|
832
|
-
this.pushState(result.context.url.pathname + result.context.url.search);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
this.pushState(url, options.replace);
|
|
836
|
-
}
|
|
837
|
-
async render(options = {}) {
|
|
838
|
-
const previous = options.previous ?? this.state.layers;
|
|
839
|
-
const url = options.url ?? this.url;
|
|
840
|
-
this.transitioning = { to: url };
|
|
841
|
-
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
842
|
-
previous,
|
|
843
|
-
state: this.state
|
|
844
|
-
});
|
|
845
|
-
if (result.redirect) return await this.render({ url: result.redirect });
|
|
846
|
-
this.transitioning = void 0;
|
|
847
|
-
return result;
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Get embedded layers from the server.
|
|
851
|
-
*/
|
|
852
|
-
getHydrationState() {
|
|
853
|
-
try {
|
|
854
|
-
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
855
|
-
} catch (error) {
|
|
856
|
-
console.error(error);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
ready = $hook({
|
|
860
|
-
on: "ready",
|
|
861
|
-
handler: async () => {
|
|
862
|
-
const hydration = this.getHydrationState();
|
|
863
|
-
const previous = hydration?.layers ?? [];
|
|
864
|
-
if (hydration) {
|
|
865
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
|
|
866
|
-
}
|
|
867
|
-
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
|
|
868
|
-
...link,
|
|
869
|
-
prefix: hydration.links.prefix
|
|
870
|
-
});
|
|
871
|
-
const { context } = await this.render({ previous });
|
|
872
|
-
await this.alepha.emit("react:browser:render", {
|
|
873
|
-
state: this.state,
|
|
874
|
-
context,
|
|
875
|
-
hydration
|
|
876
|
-
});
|
|
877
|
-
window.addEventListener("popstate", () => {
|
|
878
|
-
if (this.state.pathname === this.url) return;
|
|
879
|
-
this.render();
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
};
|
|
884
|
-
|
|
885
809
|
//#endregion
|
|
886
810
|
//#region src/providers/ReactServerProvider.ts
|
|
887
|
-
const envSchema = t.object({
|
|
811
|
+
const envSchema$1 = t.object({
|
|
888
812
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
889
813
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
890
814
|
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
@@ -894,11 +818,12 @@ const envSchema = t.object({
|
|
|
894
818
|
var ReactServerProvider = class {
|
|
895
819
|
log = $logger();
|
|
896
820
|
alepha = $inject(Alepha);
|
|
897
|
-
|
|
821
|
+
pageApi = $inject(ReactPageProvider);
|
|
822
|
+
serverProvider = $inject(ServerProvider);
|
|
898
823
|
serverStaticProvider = $inject(ServerStaticProvider);
|
|
899
824
|
serverRouterProvider = $inject(ServerRouterProvider);
|
|
900
825
|
serverTimingProvider = $inject(ServerTimingProvider);
|
|
901
|
-
env = $env(envSchema);
|
|
826
|
+
env = $env(envSchema$1);
|
|
902
827
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
903
828
|
onConfigure = $hook({
|
|
904
829
|
on: "configure",
|
|
@@ -906,7 +831,23 @@ var ReactServerProvider = class {
|
|
|
906
831
|
const pages = this.alepha.descriptors($page);
|
|
907
832
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
908
833
|
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
909
|
-
for (const page of pages)
|
|
834
|
+
for (const page of pages) {
|
|
835
|
+
page.render = this.createRenderFunction(page.name);
|
|
836
|
+
page.fetch = async (options) => {
|
|
837
|
+
const response = await fetch(`${this.serverProvider.hostname}/${page.pathname(options)}`);
|
|
838
|
+
const html = await response.text();
|
|
839
|
+
if (options?.html) return {
|
|
840
|
+
html,
|
|
841
|
+
response
|
|
842
|
+
};
|
|
843
|
+
const match = html.match(this.ROOT_DIV_REGEX);
|
|
844
|
+
if (match) return {
|
|
845
|
+
html: match[3],
|
|
846
|
+
response
|
|
847
|
+
};
|
|
848
|
+
throw new AlephaError("Invalid HTML response");
|
|
849
|
+
};
|
|
850
|
+
}
|
|
910
851
|
if (this.alepha.isServerless() === "vite") {
|
|
911
852
|
await this.configureVite(ssrEnabled);
|
|
912
853
|
return;
|
|
@@ -945,7 +886,7 @@ var ReactServerProvider = class {
|
|
|
945
886
|
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
946
887
|
}
|
|
947
888
|
async registerPages(templateLoader) {
|
|
948
|
-
for (const page of this.
|
|
889
|
+
for (const page of this.pageApi.getPages()) {
|
|
949
890
|
if (page.children?.length) continue;
|
|
950
891
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
951
892
|
this.serverRouterProvider.createRoute({
|
|
@@ -979,25 +920,39 @@ var ReactServerProvider = class {
|
|
|
979
920
|
*/
|
|
980
921
|
createRenderFunction(name, withIndex = false) {
|
|
981
922
|
return async (options = {}) => {
|
|
982
|
-
const page = this.
|
|
983
|
-
const url = new URL(this.
|
|
984
|
-
const
|
|
923
|
+
const page = this.pageApi.page(name);
|
|
924
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
925
|
+
const entry = {
|
|
985
926
|
url,
|
|
986
927
|
params: options.params ?? {},
|
|
987
928
|
query: options.query ?? {},
|
|
988
|
-
|
|
989
|
-
|
|
929
|
+
onError: () => null,
|
|
930
|
+
layers: [],
|
|
931
|
+
meta: {}
|
|
990
932
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
933
|
+
const state = entry;
|
|
934
|
+
this.log.trace("Rendering", { url });
|
|
935
|
+
await this.alepha.emit("react:server:render:begin", { state });
|
|
936
|
+
const { redirect } = await this.pageApi.createLayers(page, state);
|
|
937
|
+
if (redirect) return {
|
|
938
|
+
state,
|
|
939
|
+
html: "",
|
|
940
|
+
redirect
|
|
941
|
+
};
|
|
942
|
+
if (!withIndex && !options.html) {
|
|
943
|
+
this.alepha.state("react.router.state", state);
|
|
944
|
+
return {
|
|
945
|
+
state,
|
|
946
|
+
html: renderToString(this.pageApi.root(state))
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const html = this.renderToHtml(this.template ?? "", state, options.hydration);
|
|
950
|
+
if (html instanceof Redirection) return {
|
|
951
|
+
state,
|
|
952
|
+
html: "",
|
|
953
|
+
redirect
|
|
996
954
|
};
|
|
997
|
-
const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
|
|
998
|
-
if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
|
|
999
955
|
const result = {
|
|
1000
|
-
context,
|
|
1001
956
|
state,
|
|
1002
957
|
html
|
|
1003
958
|
};
|
|
@@ -1005,89 +960,82 @@ var ReactServerProvider = class {
|
|
|
1005
960
|
return result;
|
|
1006
961
|
};
|
|
1007
962
|
}
|
|
1008
|
-
createHandler(
|
|
963
|
+
createHandler(route, templateLoader) {
|
|
1009
964
|
return async (serverRequest) => {
|
|
1010
965
|
const { url, reply, query, params } = serverRequest;
|
|
1011
966
|
const template = await templateLoader();
|
|
1012
967
|
if (!template) throw new Error("Template not found");
|
|
1013
|
-
this.log.trace("Rendering page", { name:
|
|
1014
|
-
const
|
|
968
|
+
this.log.trace("Rendering page", { name: route.name });
|
|
969
|
+
const entry = {
|
|
1015
970
|
url,
|
|
1016
971
|
params,
|
|
1017
972
|
query,
|
|
1018
|
-
|
|
1019
|
-
|
|
973
|
+
onError: () => null,
|
|
974
|
+
layers: []
|
|
1020
975
|
};
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}));
|
|
1028
|
-
this.alepha.context.set("links", context.links);
|
|
1029
|
-
}
|
|
1030
|
-
let target = page;
|
|
976
|
+
const state = entry;
|
|
977
|
+
if (this.alepha.has(ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
978
|
+
user: serverRequest.user,
|
|
979
|
+
authorization: serverRequest.headers.authorization
|
|
980
|
+
}));
|
|
981
|
+
let target = route;
|
|
1031
982
|
while (target) {
|
|
1032
|
-
if (
|
|
983
|
+
if (route.can && !route.can()) {
|
|
1033
984
|
reply.status = 403;
|
|
1034
985
|
reply.headers["content-type"] = "text/plain";
|
|
1035
986
|
return "Forbidden";
|
|
1036
987
|
}
|
|
1037
988
|
target = target.parent;
|
|
1038
989
|
}
|
|
1039
|
-
await this.alepha.emit("react:transition:begin", {
|
|
1040
|
-
request: serverRequest,
|
|
1041
|
-
context
|
|
1042
|
-
});
|
|
1043
990
|
await this.alepha.emit("react:server:render:begin", {
|
|
1044
991
|
request: serverRequest,
|
|
1045
|
-
|
|
992
|
+
state
|
|
1046
993
|
});
|
|
1047
994
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
1048
|
-
const
|
|
995
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
1049
996
|
this.serverTimingProvider.endTiming("createLayers");
|
|
1050
|
-
if (
|
|
997
|
+
if (redirect) return reply.redirect(redirect);
|
|
1051
998
|
reply.headers["content-type"] = "text/html";
|
|
1052
999
|
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
1053
1000
|
reply.headers.pragma = "no-cache";
|
|
1054
1001
|
reply.headers.expires = "0";
|
|
1055
|
-
|
|
1056
|
-
const html = this.renderToHtml(template, state, context);
|
|
1002
|
+
const html = this.renderToHtml(template, state);
|
|
1057
1003
|
if (html instanceof Redirection) {
|
|
1058
|
-
reply.redirect(typeof html.
|
|
1004
|
+
reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
|
|
1059
1005
|
return;
|
|
1060
1006
|
}
|
|
1061
1007
|
const event = {
|
|
1062
1008
|
request: serverRequest,
|
|
1063
|
-
context,
|
|
1064
1009
|
state,
|
|
1065
1010
|
html
|
|
1066
1011
|
};
|
|
1067
1012
|
await this.alepha.emit("react:server:render:end", event);
|
|
1068
|
-
|
|
1069
|
-
this.log.trace("Page rendered", { name:
|
|
1013
|
+
route.onServerResponse?.(serverRequest);
|
|
1014
|
+
this.log.trace("Page rendered", { name: route.name });
|
|
1070
1015
|
return event.html;
|
|
1071
1016
|
};
|
|
1072
1017
|
}
|
|
1073
|
-
renderToHtml(template, state,
|
|
1074
|
-
const element = this.
|
|
1018
|
+
renderToHtml(template, state, hydration = true) {
|
|
1019
|
+
const element = this.pageApi.root(state);
|
|
1020
|
+
this.alepha.state("react.router.state", state);
|
|
1075
1021
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
1076
1022
|
let app = "";
|
|
1077
1023
|
try {
|
|
1078
1024
|
app = renderToString(element);
|
|
1079
1025
|
} catch (error) {
|
|
1080
|
-
this.log.error("
|
|
1081
|
-
const element$1 =
|
|
1026
|
+
this.log.error("renderToString has failed, fallback to error handler", error);
|
|
1027
|
+
const element$1 = state.onError(error, state);
|
|
1082
1028
|
if (element$1 instanceof Redirection) return element$1;
|
|
1083
1029
|
app = renderToString(element$1);
|
|
1030
|
+
this.log.debug("Error handled successfully with fallback");
|
|
1084
1031
|
}
|
|
1085
1032
|
this.serverTimingProvider.endTiming("renderToString");
|
|
1086
1033
|
const response = { html: template };
|
|
1087
1034
|
if (hydration) {
|
|
1088
|
-
const { request, context
|
|
1035
|
+
const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
|
|
1089
1036
|
const hydrationData = {
|
|
1090
|
-
...
|
|
1037
|
+
...store,
|
|
1038
|
+
"react.router.state": void 0,
|
|
1091
1039
|
layers: state.layers.map((it) => ({
|
|
1092
1040
|
...it,
|
|
1093
1041
|
error: it.error ? {
|
|
@@ -1102,7 +1050,7 @@ var ReactServerProvider = class {
|
|
|
1102
1050
|
route: void 0
|
|
1103
1051
|
}))
|
|
1104
1052
|
};
|
|
1105
|
-
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}
|
|
1053
|
+
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
|
|
1106
1054
|
this.fillTemplate(response, app, script);
|
|
1107
1055
|
}
|
|
1108
1056
|
return response.html;
|
|
@@ -1123,27 +1071,262 @@ var ReactServerProvider = class {
|
|
|
1123
1071
|
};
|
|
1124
1072
|
|
|
1125
1073
|
//#endregion
|
|
1126
|
-
//#region src/
|
|
1127
|
-
var
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
this.
|
|
1133
|
-
|
|
1134
|
-
|
|
1074
|
+
//#region src/providers/ReactBrowserRouterProvider.ts
|
|
1075
|
+
var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
1076
|
+
log = $logger();
|
|
1077
|
+
alepha = $inject(Alepha);
|
|
1078
|
+
pageApi = $inject(ReactPageProvider);
|
|
1079
|
+
add(entry) {
|
|
1080
|
+
this.pageApi.add(entry);
|
|
1081
|
+
}
|
|
1082
|
+
configure = $hook({
|
|
1083
|
+
on: "configure",
|
|
1084
|
+
handler: async () => {
|
|
1085
|
+
for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
|
|
1086
|
+
path: page.match,
|
|
1087
|
+
page
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
async transition(url, previous = [], meta = {}) {
|
|
1092
|
+
const { pathname, search } = url;
|
|
1093
|
+
const entry = {
|
|
1094
|
+
url,
|
|
1095
|
+
query: {},
|
|
1096
|
+
params: {},
|
|
1097
|
+
layers: [],
|
|
1098
|
+
onError: () => null,
|
|
1099
|
+
meta
|
|
1100
|
+
};
|
|
1101
|
+
const state = entry;
|
|
1102
|
+
await this.alepha.emit("react:transition:begin", {
|
|
1103
|
+
previous: this.alepha.state("react.router.state"),
|
|
1104
|
+
state
|
|
1105
|
+
});
|
|
1106
|
+
try {
|
|
1107
|
+
const { route, params } = this.match(pathname);
|
|
1108
|
+
const query = {};
|
|
1109
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
1110
|
+
state.query = query;
|
|
1111
|
+
state.params = params ?? {};
|
|
1112
|
+
if (isPageRoute(route)) {
|
|
1113
|
+
const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
|
|
1114
|
+
if (redirect) return redirect;
|
|
1115
|
+
}
|
|
1116
|
+
if (state.layers.length === 0) state.layers.push({
|
|
1117
|
+
name: "not-found",
|
|
1118
|
+
element: createElement(NotFoundPage),
|
|
1119
|
+
index: 0,
|
|
1120
|
+
path: "/"
|
|
1121
|
+
});
|
|
1122
|
+
await this.alepha.emit("react:transition:success", { state });
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
this.log.error("Transition has failed", e);
|
|
1125
|
+
state.layers = [{
|
|
1126
|
+
name: "error",
|
|
1127
|
+
element: this.pageApi.renderError(e),
|
|
1128
|
+
index: 0,
|
|
1129
|
+
path: "/"
|
|
1130
|
+
}];
|
|
1131
|
+
await this.alepha.emit("react:transition:error", {
|
|
1132
|
+
error: e,
|
|
1133
|
+
state
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (previous) for (let i = 0; i < previous.length; i++) {
|
|
1137
|
+
const layer = previous[i];
|
|
1138
|
+
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
1139
|
+
}
|
|
1140
|
+
this.alepha.state("react.router.state", state);
|
|
1141
|
+
await this.alepha.emit("react:transition:end", { state });
|
|
1142
|
+
}
|
|
1143
|
+
root(state) {
|
|
1144
|
+
return this.pageApi.root(state);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
1150
|
+
const envSchema = t.object({ REACT_ROOT_ID: t.string({ default: "root" }) });
|
|
1151
|
+
var ReactBrowserProvider = class {
|
|
1152
|
+
env = $env(envSchema);
|
|
1153
|
+
log = $logger();
|
|
1154
|
+
client = $inject(LinkProvider);
|
|
1155
|
+
alepha = $inject(Alepha);
|
|
1156
|
+
router = $inject(ReactBrowserRouterProvider);
|
|
1157
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
1158
|
+
options = { scrollRestoration: "top" };
|
|
1159
|
+
getRootElement() {
|
|
1160
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
1161
|
+
if (root) return root;
|
|
1162
|
+
const div = this.document.createElement("div");
|
|
1163
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
1164
|
+
this.document.body.prepend(div);
|
|
1165
|
+
return div;
|
|
1166
|
+
}
|
|
1167
|
+
transitioning;
|
|
1168
|
+
get state() {
|
|
1169
|
+
return this.alepha.state("react.router.state");
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Accessor for Document DOM API.
|
|
1173
|
+
*/
|
|
1174
|
+
get document() {
|
|
1175
|
+
return window.document;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Accessor for History DOM API.
|
|
1179
|
+
*/
|
|
1180
|
+
get history() {
|
|
1181
|
+
return window.history;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Accessor for Location DOM API.
|
|
1185
|
+
*/
|
|
1186
|
+
get location() {
|
|
1187
|
+
return window.location;
|
|
1188
|
+
}
|
|
1189
|
+
get base() {
|
|
1190
|
+
const base = import.meta.env?.BASE_URL;
|
|
1191
|
+
if (!base || base === "/") return "";
|
|
1192
|
+
return base;
|
|
1193
|
+
}
|
|
1194
|
+
get url() {
|
|
1195
|
+
const url = this.location.pathname + this.location.search;
|
|
1196
|
+
if (this.base) return url.replace(this.base, "");
|
|
1197
|
+
return url;
|
|
1198
|
+
}
|
|
1199
|
+
pushState(path, replace) {
|
|
1200
|
+
const url = this.base + path;
|
|
1201
|
+
if (replace) this.history.replaceState({}, "", url);
|
|
1202
|
+
else this.history.pushState({}, "", url);
|
|
1203
|
+
}
|
|
1204
|
+
async invalidate(props) {
|
|
1205
|
+
const previous = [];
|
|
1206
|
+
this.log.trace("Invalidating layers");
|
|
1207
|
+
if (props) {
|
|
1208
|
+
const [key] = Object.keys(props);
|
|
1209
|
+
const value = props[key];
|
|
1210
|
+
for (const layer of this.state.layers) {
|
|
1211
|
+
if (layer.props?.[key]) {
|
|
1212
|
+
previous.push({
|
|
1213
|
+
...layer,
|
|
1214
|
+
props: {
|
|
1215
|
+
...layer.props,
|
|
1216
|
+
[key]: value
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
previous.push(layer);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
await this.render({ previous });
|
|
1225
|
+
}
|
|
1226
|
+
async go(url, options = {}) {
|
|
1227
|
+
this.log.trace(`Going to ${url}`, {
|
|
1228
|
+
url,
|
|
1229
|
+
options
|
|
1230
|
+
});
|
|
1231
|
+
await this.render({
|
|
1232
|
+
url,
|
|
1233
|
+
previous: options.force ? [] : this.state.layers,
|
|
1234
|
+
meta: options.meta
|
|
1235
|
+
});
|
|
1236
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1237
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
this.pushState(url, options.replace);
|
|
1241
|
+
}
|
|
1242
|
+
async render(options = {}) {
|
|
1243
|
+
const previous = options.previous ?? this.state.layers;
|
|
1244
|
+
const url = options.url ?? this.url;
|
|
1245
|
+
const start = this.dateTimeProvider.now();
|
|
1246
|
+
this.transitioning = {
|
|
1247
|
+
to: url,
|
|
1248
|
+
from: this.state?.url.pathname
|
|
1249
|
+
};
|
|
1250
|
+
this.log.debug("Transitioning...", { to: url });
|
|
1251
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
|
|
1252
|
+
if (redirect) {
|
|
1253
|
+
this.log.info("Redirecting to", { redirect });
|
|
1254
|
+
return await this.render({ url: redirect });
|
|
1255
|
+
}
|
|
1256
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
1257
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
1258
|
+
this.transitioning = void 0;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Get embedded layers from the server.
|
|
1262
|
+
*/
|
|
1263
|
+
getHydrationState() {
|
|
1264
|
+
try {
|
|
1265
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
console.error(error);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
onTransitionEnd = $hook({
|
|
1271
|
+
on: "react:transition:end",
|
|
1272
|
+
handler: () => {
|
|
1273
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
|
|
1274
|
+
this.log.trace("Restoring scroll position to top");
|
|
1275
|
+
window.scrollTo(0, 0);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
ready = $hook({
|
|
1280
|
+
on: "ready",
|
|
1281
|
+
handler: async () => {
|
|
1282
|
+
const hydration = this.getHydrationState();
|
|
1283
|
+
const previous = hydration?.layers ?? [];
|
|
1284
|
+
if (hydration) {
|
|
1285
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1286
|
+
}
|
|
1287
|
+
await this.render({ previous });
|
|
1288
|
+
const element = this.router.root(this.state);
|
|
1289
|
+
await this.alepha.emit("react:browser:render", {
|
|
1290
|
+
element,
|
|
1291
|
+
root: this.getRootElement(),
|
|
1292
|
+
hydration,
|
|
1293
|
+
state: this.state
|
|
1294
|
+
});
|
|
1295
|
+
window.addEventListener("popstate", () => {
|
|
1296
|
+
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1297
|
+
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
1298
|
+
this.render();
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/services/ReactRouter.ts
|
|
1306
|
+
var ReactRouter = class {
|
|
1307
|
+
alepha = $inject(Alepha);
|
|
1308
|
+
pageApi = $inject(ReactPageProvider);
|
|
1309
|
+
get state() {
|
|
1310
|
+
return this.alepha.state("react.router.state");
|
|
1311
|
+
}
|
|
1312
|
+
get pages() {
|
|
1313
|
+
return this.pageApi.getPages();
|
|
1314
|
+
}
|
|
1315
|
+
get browser() {
|
|
1316
|
+
if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
|
|
1317
|
+
return void 0;
|
|
1135
1318
|
}
|
|
1136
1319
|
path(name, config = {}) {
|
|
1137
1320
|
return this.pageApi.pathname(name, {
|
|
1138
1321
|
params: {
|
|
1139
|
-
...this.
|
|
1322
|
+
...this.state.params,
|
|
1140
1323
|
...config.params
|
|
1141
1324
|
},
|
|
1142
1325
|
query: config.query
|
|
1143
1326
|
});
|
|
1144
1327
|
}
|
|
1145
1328
|
getURL() {
|
|
1146
|
-
if (!this.browser) return this.
|
|
1329
|
+
if (!this.browser) return this.state.url;
|
|
1147
1330
|
return new URL(this.location.href);
|
|
1148
1331
|
}
|
|
1149
1332
|
get location() {
|
|
@@ -1154,11 +1337,11 @@ var RouterHookApi = class {
|
|
|
1154
1337
|
return this.state;
|
|
1155
1338
|
}
|
|
1156
1339
|
get pathname() {
|
|
1157
|
-
return this.state.pathname;
|
|
1340
|
+
return this.state.url.pathname;
|
|
1158
1341
|
}
|
|
1159
1342
|
get query() {
|
|
1160
1343
|
const query = {};
|
|
1161
|
-
for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
|
|
1344
|
+
for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
|
|
1162
1345
|
return query;
|
|
1163
1346
|
}
|
|
1164
1347
|
async back() {
|
|
@@ -1170,17 +1353,6 @@ var RouterHookApi = class {
|
|
|
1170
1353
|
async invalidate(props) {
|
|
1171
1354
|
await this.browser?.invalidate(props);
|
|
1172
1355
|
}
|
|
1173
|
-
/**
|
|
1174
|
-
* Create a valid href for the given pathname.
|
|
1175
|
-
*
|
|
1176
|
-
* @param pathname
|
|
1177
|
-
* @param layer
|
|
1178
|
-
*/
|
|
1179
|
-
createHref(pathname, layer = this.layer, options = {}) {
|
|
1180
|
-
if (typeof pathname === "object") pathname = pathname.options.path ?? "";
|
|
1181
|
-
if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
|
|
1182
|
-
return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
1183
|
-
}
|
|
1184
1356
|
async go(path, options) {
|
|
1185
1357
|
for (const page of this.pages) if (page.name === path) {
|
|
1186
1358
|
await this.browser?.go(this.path(path, options), options);
|
|
@@ -1195,7 +1367,7 @@ var RouterHookApi = class {
|
|
|
1195
1367
|
break;
|
|
1196
1368
|
}
|
|
1197
1369
|
return {
|
|
1198
|
-
href,
|
|
1370
|
+
href: this.base(href),
|
|
1199
1371
|
onClick: (ev) => {
|
|
1200
1372
|
ev.stopPropagation();
|
|
1201
1373
|
ev.preventDefault();
|
|
@@ -1203,6 +1375,11 @@ var RouterHookApi = class {
|
|
|
1203
1375
|
}
|
|
1204
1376
|
};
|
|
1205
1377
|
}
|
|
1378
|
+
base(path) {
|
|
1379
|
+
const base = import.meta.env?.BASE_URL;
|
|
1380
|
+
if (!base || base === "/") return path;
|
|
1381
|
+
return base + path;
|
|
1382
|
+
}
|
|
1206
1383
|
/**
|
|
1207
1384
|
* Set query params.
|
|
1208
1385
|
*
|
|
@@ -1218,107 +1395,99 @@ var RouterHookApi = class {
|
|
|
1218
1395
|
}
|
|
1219
1396
|
};
|
|
1220
1397
|
|
|
1398
|
+
//#endregion
|
|
1399
|
+
//#region src/hooks/useInject.ts
|
|
1400
|
+
/**
|
|
1401
|
+
* Hook to inject a service instance.
|
|
1402
|
+
* It's a wrapper of `useAlepha().inject(service)` with a memoization.
|
|
1403
|
+
*/
|
|
1404
|
+
const useInject = (service) => {
|
|
1405
|
+
const alepha = useAlepha();
|
|
1406
|
+
return useMemo(() => alepha.inject(service), []);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1221
1409
|
//#endregion
|
|
1222
1410
|
//#region src/hooks/useRouter.ts
|
|
1411
|
+
/**
|
|
1412
|
+
* Use this hook to access the React Router instance.
|
|
1413
|
+
*
|
|
1414
|
+
* You can add a type parameter to specify the type of your application.
|
|
1415
|
+
* This will allow you to use the router in a typesafe way.
|
|
1416
|
+
*
|
|
1417
|
+
* @example
|
|
1418
|
+
* class App {
|
|
1419
|
+
* home = $page();
|
|
1420
|
+
* }
|
|
1421
|
+
*
|
|
1422
|
+
* const router = useRouter<App>();
|
|
1423
|
+
* router.go("home"); // typesafe
|
|
1424
|
+
*/
|
|
1223
1425
|
const useRouter = () => {
|
|
1224
|
-
|
|
1225
|
-
const ctx = useContext(RouterContext);
|
|
1226
|
-
const layer = useContext(RouterLayerContext);
|
|
1227
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1228
|
-
const pages = useMemo(() => {
|
|
1229
|
-
return alepha.inject(PageDescriptorProvider).getPages();
|
|
1230
|
-
}, []);
|
|
1231
|
-
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
1426
|
+
return useInject(ReactRouter);
|
|
1232
1427
|
};
|
|
1233
1428
|
|
|
1234
1429
|
//#endregion
|
|
1235
1430
|
//#region src/components/Link.tsx
|
|
1236
1431
|
const Link = (props) => {
|
|
1237
1432
|
const router = useRouter();
|
|
1238
|
-
const { to,...anchorProps } = props;
|
|
1239
1433
|
return /* @__PURE__ */ jsx("a", {
|
|
1240
|
-
...
|
|
1241
|
-
...
|
|
1434
|
+
...props,
|
|
1435
|
+
...router.anchor(props.href),
|
|
1242
1436
|
children: props.children
|
|
1243
1437
|
});
|
|
1244
1438
|
};
|
|
1245
|
-
var Link_default = Link;
|
|
1246
1439
|
|
|
1247
1440
|
//#endregion
|
|
1248
1441
|
//#region src/hooks/useActive.ts
|
|
1249
|
-
const useActive = (
|
|
1442
|
+
const useActive = (args) => {
|
|
1250
1443
|
const router = useRouter();
|
|
1251
|
-
const ctx = useContext(RouterContext);
|
|
1252
|
-
const layer = useContext(RouterLayerContext);
|
|
1253
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1254
|
-
const [current, setCurrent] = useState(ctx.state.pathname);
|
|
1255
|
-
const href = useMemo(() => router.createHref(path ?? "", layer), [path, layer]);
|
|
1256
1444
|
const [isPending, setPending] = useState(false);
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1445
|
+
const state = useRouterState();
|
|
1446
|
+
const current = state.url.pathname;
|
|
1447
|
+
const options = typeof args === "string" ? { href: args } : {
|
|
1448
|
+
...args,
|
|
1449
|
+
href: args.href
|
|
1450
|
+
};
|
|
1451
|
+
const href = options.href;
|
|
1452
|
+
let isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1453
|
+
if (options.startWith && !isActive) isActive = current.startsWith(href);
|
|
1261
1454
|
return {
|
|
1262
1455
|
isPending,
|
|
1263
1456
|
isActive,
|
|
1264
1457
|
anchorProps: {
|
|
1265
|
-
href,
|
|
1266
|
-
onClick: (ev) => {
|
|
1458
|
+
href: router.base(href),
|
|
1459
|
+
onClick: async (ev) => {
|
|
1267
1460
|
ev?.stopPropagation();
|
|
1268
1461
|
ev?.preventDefault();
|
|
1269
1462
|
if (isActive) return;
|
|
1270
1463
|
if (isPending) return;
|
|
1271
1464
|
setPending(true);
|
|
1272
|
-
|
|
1465
|
+
try {
|
|
1466
|
+
await router.go(href);
|
|
1467
|
+
} finally {
|
|
1273
1468
|
setPending(false);
|
|
1274
|
-
}
|
|
1469
|
+
}
|
|
1275
1470
|
}
|
|
1276
1471
|
}
|
|
1277
1472
|
};
|
|
1278
1473
|
};
|
|
1279
1474
|
|
|
1280
1475
|
//#endregion
|
|
1281
|
-
//#region src/hooks/
|
|
1282
|
-
const useInject = (service) => {
|
|
1283
|
-
const alepha = useAlepha();
|
|
1284
|
-
return useMemo(() => alepha.inject(service), []);
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
|
-
//#endregion
|
|
1288
|
-
//#region src/hooks/useStore.ts
|
|
1476
|
+
//#region src/hooks/useClient.ts
|
|
1289
1477
|
/**
|
|
1290
|
-
* Hook to
|
|
1478
|
+
* Hook to get a virtual client for the specified scope.
|
|
1479
|
+
*
|
|
1480
|
+
* It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
|
|
1291
1481
|
*/
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
useMemo(() => {
|
|
1295
|
-
if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
|
|
1296
|
-
}, [defaultValue]);
|
|
1297
|
-
const [state, setState] = useState(alepha.state(key));
|
|
1298
|
-
useEffect(() => {
|
|
1299
|
-
if (!alepha.isBrowser()) return;
|
|
1300
|
-
return alepha.on("state:mutate", (ev) => {
|
|
1301
|
-
if (ev.key === key) setState(ev.value);
|
|
1302
|
-
});
|
|
1303
|
-
}, []);
|
|
1304
|
-
if (!alepha.isBrowser()) {
|
|
1305
|
-
const value = alepha.context.get(key);
|
|
1306
|
-
if (value !== null) return [value, (_) => {}];
|
|
1307
|
-
}
|
|
1308
|
-
return [state, (value) => {
|
|
1309
|
-
alepha.state(key, value);
|
|
1310
|
-
}];
|
|
1311
|
-
};
|
|
1312
|
-
|
|
1313
|
-
//#endregion
|
|
1314
|
-
//#region src/hooks/useClient.ts
|
|
1315
|
-
const useClient = (_scope) => {
|
|
1316
|
-
useStore("user");
|
|
1317
|
-
return useInject(LinkProvider).client();
|
|
1482
|
+
const useClient = (scope) => {
|
|
1483
|
+
return useInject(LinkProvider).client(scope);
|
|
1318
1484
|
};
|
|
1319
1485
|
|
|
1320
1486
|
//#endregion
|
|
1321
1487
|
//#region src/hooks/useQueryParams.ts
|
|
1488
|
+
/**
|
|
1489
|
+
* Not well tested. Use with caution.
|
|
1490
|
+
*/
|
|
1322
1491
|
const useQueryParams = (schema, options = {}) => {
|
|
1323
1492
|
const alepha = useAlepha();
|
|
1324
1493
|
const key = options.key ?? "q";
|
|
@@ -1349,29 +1518,17 @@ const decode = (alepha, schema, data) => {
|
|
|
1349
1518
|
}
|
|
1350
1519
|
};
|
|
1351
1520
|
|
|
1352
|
-
//#endregion
|
|
1353
|
-
//#region src/hooks/useRouterState.ts
|
|
1354
|
-
const useRouterState = () => {
|
|
1355
|
-
const router = useContext(RouterContext);
|
|
1356
|
-
const layer = useContext(RouterLayerContext);
|
|
1357
|
-
if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
|
|
1358
|
-
const [state, setState] = useState(router.state);
|
|
1359
|
-
useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
|
|
1360
|
-
return state;
|
|
1361
|
-
};
|
|
1362
|
-
|
|
1363
1521
|
//#endregion
|
|
1364
1522
|
//#region src/hooks/useSchema.ts
|
|
1365
1523
|
const useSchema = (action) => {
|
|
1366
1524
|
const name = action.name;
|
|
1367
1525
|
const alepha = useAlepha();
|
|
1368
1526
|
const httpClient = useInject(HttpClient);
|
|
1369
|
-
const linkProvider = useInject(LinkProvider);
|
|
1370
1527
|
const [schema, setSchema] = useState(ssrSchemaLoading(alepha, name));
|
|
1371
1528
|
useEffect(() => {
|
|
1372
1529
|
if (!schema.loading) return;
|
|
1373
1530
|
const opts = { cache: true };
|
|
1374
|
-
httpClient.fetch(`${
|
|
1531
|
+
httpClient.fetch(`${LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
|
|
1375
1532
|
}, [name]);
|
|
1376
1533
|
return schema;
|
|
1377
1534
|
};
|
|
@@ -1380,10 +1537,10 @@ const useSchema = (action) => {
|
|
|
1380
1537
|
*/
|
|
1381
1538
|
const ssrSchemaLoading = (alepha, name) => {
|
|
1382
1539
|
if (!alepha.isBrowser()) {
|
|
1383
|
-
const
|
|
1384
|
-
const can =
|
|
1540
|
+
const linkProvider = alepha.inject(LinkProvider);
|
|
1541
|
+
const can = linkProvider.getServerLinks().find((link) => link.name === name);
|
|
1385
1542
|
if (can) {
|
|
1386
|
-
const schema$1 =
|
|
1543
|
+
const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
|
|
1387
1544
|
if (schema$1) {
|
|
1388
1545
|
can.schema = schema$1;
|
|
1389
1546
|
return schema$1;
|
|
@@ -1391,7 +1548,7 @@ const ssrSchemaLoading = (alepha, name) => {
|
|
|
1391
1548
|
}
|
|
1392
1549
|
return { loading: true };
|
|
1393
1550
|
}
|
|
1394
|
-
const schema = alepha.inject(LinkProvider).links
|
|
1551
|
+
const schema = alepha.inject(LinkProvider).links.find((it) => it.name === name)?.schema;
|
|
1395
1552
|
if (schema) return schema;
|
|
1396
1553
|
return { loading: true };
|
|
1397
1554
|
};
|
|
@@ -1413,12 +1570,12 @@ const AlephaReact = $module({
|
|
|
1413
1570
|
descriptors: [$page],
|
|
1414
1571
|
services: [
|
|
1415
1572
|
ReactServerProvider,
|
|
1416
|
-
|
|
1417
|
-
|
|
1573
|
+
ReactPageProvider,
|
|
1574
|
+
ReactRouter
|
|
1418
1575
|
],
|
|
1419
|
-
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(
|
|
1576
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
|
|
1420
1577
|
});
|
|
1421
1578
|
|
|
1422
1579
|
//#endregion
|
|
1423
|
-
export { $page, AlephaContext, AlephaReact,
|
|
1580
|
+
export { $page, AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, ErrorViewer, Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactPageProvider, ReactRouter, ReactServerProvider, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
|
|
1424
1581
|
//# sourceMappingURL=index.js.map
|