@alepha/react 0.8.0 → 0.9.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 +32 -1
- package/dist/index.browser.js +88 -45
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +277 -247
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -72
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +71 -73
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +282 -249
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/components/Link.tsx +3 -5
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +39 -58
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useInject.ts +1 -1
- package/src/hooks/useRouter.ts +3 -2
- package/src/index.browser.ts +13 -8
- package/src/index.shared.ts +1 -0
- package/src/index.ts +16 -12
- package/src/providers/PageDescriptorProvider.ts +49 -36
- package/src/providers/ReactBrowserProvider.ts +37 -4
- package/src/providers/ReactBrowserRenderer.ts +22 -2
- package/src/providers/ReactServerProvider.ts +13 -15
package/dist/index.js
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import { $hook, $inject, $logger,
|
|
1
|
+
import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, NotImplementedError, createDescriptor, t } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, ServerRouterProvider, ServerTimingProvider, apiLinksResponseSchema } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
5
5
|
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { RouterProvider } from "@alepha/router";
|
|
7
8
|
import { existsSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
10
11
|
import { renderToString } from "react-dom/server";
|
|
11
|
-
import { RouterProvider } from "@alepha/router";
|
|
12
12
|
|
|
13
13
|
//#region src/descriptors/$page.ts
|
|
14
|
-
const KEY = "PAGE";
|
|
15
14
|
/**
|
|
16
15
|
* Main descriptor for defining a React route in the application.
|
|
17
16
|
*/
|
|
18
17
|
const $page = (options) => {
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
[KIND]: KEY,
|
|
22
|
-
[OPTIONS]: options,
|
|
23
|
-
render: () => {
|
|
24
|
-
throw new NotImplementedError(KEY);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
18
|
+
return createDescriptor(PageDescriptor, options);
|
|
27
19
|
};
|
|
28
|
-
|
|
20
|
+
var PageDescriptor = class extends Descriptor {
|
|
21
|
+
get name() {
|
|
22
|
+
return this.options.name ?? this.config.propertyKey;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
26
|
+
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
27
|
+
*/
|
|
28
|
+
async render(options) {
|
|
29
|
+
throw new NotImplementedError("");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
$page[KIND] = PageDescriptor;
|
|
29
33
|
|
|
30
34
|
//#endregion
|
|
31
35
|
//#region src/components/ClientOnly.tsx
|
|
@@ -291,7 +295,7 @@ const NestedView = (props) => {
|
|
|
291
295
|
const index = layer?.index ?? 0;
|
|
292
296
|
const [view, setView] = useState(app?.state.layers[index]?.element);
|
|
293
297
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
294
|
-
setView(state.layers[index]?.element);
|
|
298
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
295
299
|
} }, [app]);
|
|
296
300
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
297
301
|
const element = view ?? props.children ?? null;
|
|
@@ -341,7 +345,7 @@ var RedirectionError = class extends Error {
|
|
|
341
345
|
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
342
346
|
var PageDescriptorProvider = class {
|
|
343
347
|
log = $logger();
|
|
344
|
-
env = $
|
|
348
|
+
env = $env(envSchema$1);
|
|
345
349
|
alepha = $inject(Alepha);
|
|
346
350
|
pages = [];
|
|
347
351
|
getPages() {
|
|
@@ -389,19 +393,18 @@ var PageDescriptorProvider = class {
|
|
|
389
393
|
const route$1 = it.route;
|
|
390
394
|
const config = {};
|
|
391
395
|
try {
|
|
392
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) :
|
|
396
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
|
|
393
397
|
} catch (e) {
|
|
394
398
|
it.error = e;
|
|
395
399
|
break;
|
|
396
400
|
}
|
|
397
401
|
try {
|
|
398
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) :
|
|
402
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
|
|
399
403
|
} catch (e) {
|
|
400
404
|
it.error = e;
|
|
401
405
|
break;
|
|
402
406
|
}
|
|
403
407
|
it.config = { ...config };
|
|
404
|
-
if (!route$1.resolve) continue;
|
|
405
408
|
const previous = request.previous;
|
|
406
409
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
407
410
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -416,6 +419,7 @@ var PageDescriptorProvider = class {
|
|
|
416
419
|
if (prev === curr) {
|
|
417
420
|
it.props = previous[i].props;
|
|
418
421
|
it.error = previous[i].error;
|
|
422
|
+
it.cache = true;
|
|
419
423
|
context = {
|
|
420
424
|
...context,
|
|
421
425
|
...it.props
|
|
@@ -424,6 +428,7 @@ var PageDescriptorProvider = class {
|
|
|
424
428
|
}
|
|
425
429
|
forceRefresh = true;
|
|
426
430
|
}
|
|
431
|
+
if (!route$1.resolve) continue;
|
|
427
432
|
try {
|
|
428
433
|
const props = await route$1.resolve?.({
|
|
429
434
|
...request,
|
|
@@ -470,7 +475,7 @@ var PageDescriptorProvider = class {
|
|
|
470
475
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
471
476
|
index: i + 1,
|
|
472
477
|
path,
|
|
473
|
-
route
|
|
478
|
+
route: it.route
|
|
474
479
|
});
|
|
475
480
|
break;
|
|
476
481
|
}
|
|
@@ -486,7 +491,8 @@ var PageDescriptorProvider = class {
|
|
|
486
491
|
element: this.renderView(i + 1, path, element, it.route),
|
|
487
492
|
index: i + 1,
|
|
488
493
|
path,
|
|
489
|
-
route
|
|
494
|
+
route: it.route,
|
|
495
|
+
cache: it.cache
|
|
490
496
|
});
|
|
491
497
|
}
|
|
492
498
|
return {
|
|
@@ -545,18 +551,17 @@ var PageDescriptorProvider = class {
|
|
|
545
551
|
on: "configure",
|
|
546
552
|
handler: () => {
|
|
547
553
|
let hasNotFoundHandler = false;
|
|
548
|
-
const pages = this.alepha.
|
|
554
|
+
const pages = this.alepha.descriptors($page);
|
|
549
555
|
const hasParent = (it) => {
|
|
550
556
|
for (const page of pages) {
|
|
551
|
-
const children = page.
|
|
557
|
+
const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
|
|
552
558
|
if (children.includes(it)) return true;
|
|
553
559
|
}
|
|
554
560
|
};
|
|
555
|
-
for (const
|
|
556
|
-
|
|
557
|
-
if (hasParent(
|
|
558
|
-
|
|
559
|
-
this.add(this.map(pages, value));
|
|
561
|
+
for (const page of pages) {
|
|
562
|
+
if (page.options.path === "/*") hasNotFoundHandler = true;
|
|
563
|
+
if (hasParent(page)) continue;
|
|
564
|
+
this.add(this.map(pages, page));
|
|
560
565
|
}
|
|
561
566
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
562
567
|
path: "/*",
|
|
@@ -570,9 +575,10 @@ var PageDescriptorProvider = class {
|
|
|
570
575
|
}
|
|
571
576
|
});
|
|
572
577
|
map(pages, target) {
|
|
573
|
-
const children = target
|
|
578
|
+
const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
|
|
574
579
|
return {
|
|
575
|
-
...target
|
|
580
|
+
...target.options,
|
|
581
|
+
name: target.name,
|
|
576
582
|
parent: void 0,
|
|
577
583
|
children: children.map((it) => this.map(pages, it))
|
|
578
584
|
};
|
|
@@ -609,6 +615,214 @@ const isPageRoute = (it) => {
|
|
|
609
615
|
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
610
616
|
};
|
|
611
617
|
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/providers/BrowserRouterProvider.ts
|
|
620
|
+
var BrowserRouterProvider = class extends RouterProvider {
|
|
621
|
+
log = $logger();
|
|
622
|
+
alepha = $inject(Alepha);
|
|
623
|
+
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
624
|
+
add(entry) {
|
|
625
|
+
this.pageDescriptorProvider.add(entry);
|
|
626
|
+
}
|
|
627
|
+
configure = $hook({
|
|
628
|
+
on: "configure",
|
|
629
|
+
handler: async () => {
|
|
630
|
+
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
631
|
+
path: page.match,
|
|
632
|
+
page
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
async transition(url, options = {}) {
|
|
637
|
+
const { pathname, search } = url;
|
|
638
|
+
const state = {
|
|
639
|
+
pathname,
|
|
640
|
+
search,
|
|
641
|
+
layers: []
|
|
642
|
+
};
|
|
643
|
+
const context = {
|
|
644
|
+
url,
|
|
645
|
+
query: {},
|
|
646
|
+
params: {},
|
|
647
|
+
onError: () => null,
|
|
648
|
+
...options.context ?? {}
|
|
649
|
+
};
|
|
650
|
+
await this.alepha.emit("react:transition:begin", {
|
|
651
|
+
state,
|
|
652
|
+
context
|
|
653
|
+
});
|
|
654
|
+
try {
|
|
655
|
+
const previous = options.previous;
|
|
656
|
+
const { route, params } = this.match(pathname);
|
|
657
|
+
const query = {};
|
|
658
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
659
|
+
context.query = query;
|
|
660
|
+
context.params = params ?? {};
|
|
661
|
+
context.previous = previous;
|
|
662
|
+
if (isPageRoute(route)) {
|
|
663
|
+
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
664
|
+
if (result.redirect) return {
|
|
665
|
+
redirect: result.redirect,
|
|
666
|
+
state,
|
|
667
|
+
context
|
|
668
|
+
};
|
|
669
|
+
state.layers = result.layers;
|
|
670
|
+
}
|
|
671
|
+
if (state.layers.length === 0) state.layers.push({
|
|
672
|
+
name: "not-found",
|
|
673
|
+
element: createElement(NotFoundPage),
|
|
674
|
+
index: 0,
|
|
675
|
+
path: "/"
|
|
676
|
+
});
|
|
677
|
+
await this.alepha.emit("react:transition:success", {
|
|
678
|
+
state,
|
|
679
|
+
context
|
|
680
|
+
});
|
|
681
|
+
} catch (e) {
|
|
682
|
+
this.log.error(e);
|
|
683
|
+
state.layers = [{
|
|
684
|
+
name: "error",
|
|
685
|
+
element: this.pageDescriptorProvider.renderError(e),
|
|
686
|
+
index: 0,
|
|
687
|
+
path: "/"
|
|
688
|
+
}];
|
|
689
|
+
await this.alepha.emit("react:transition:error", {
|
|
690
|
+
error: e,
|
|
691
|
+
state,
|
|
692
|
+
context
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (options.state) {
|
|
696
|
+
options.state.layers = state.layers;
|
|
697
|
+
options.state.pathname = state.pathname;
|
|
698
|
+
options.state.search = state.search;
|
|
699
|
+
}
|
|
700
|
+
await this.alepha.emit("react:transition:end", {
|
|
701
|
+
state: options.state,
|
|
702
|
+
context
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
context,
|
|
706
|
+
state
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
root(state, context) {
|
|
710
|
+
return this.pageDescriptorProvider.root(state, context);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
716
|
+
var ReactBrowserProvider = class {
|
|
717
|
+
log = $logger();
|
|
718
|
+
client = $inject(LinkProvider);
|
|
719
|
+
alepha = $inject(Alepha);
|
|
720
|
+
router = $inject(BrowserRouterProvider);
|
|
721
|
+
root;
|
|
722
|
+
transitioning;
|
|
723
|
+
state = {
|
|
724
|
+
layers: [],
|
|
725
|
+
pathname: "",
|
|
726
|
+
search: ""
|
|
727
|
+
};
|
|
728
|
+
get document() {
|
|
729
|
+
return window.document;
|
|
730
|
+
}
|
|
731
|
+
get history() {
|
|
732
|
+
return window.history;
|
|
733
|
+
}
|
|
734
|
+
get location() {
|
|
735
|
+
return window.location;
|
|
736
|
+
}
|
|
737
|
+
get url() {
|
|
738
|
+
let url = this.location.pathname + this.location.search;
|
|
739
|
+
if (import.meta?.env?.BASE_URL) {
|
|
740
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
741
|
+
if (!url.startsWith("/")) url = `/${url}`;
|
|
742
|
+
}
|
|
743
|
+
return url;
|
|
744
|
+
}
|
|
745
|
+
pushState(url, replace) {
|
|
746
|
+
let path = url;
|
|
747
|
+
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
748
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
749
|
+
else this.history.pushState({}, "", path);
|
|
750
|
+
}
|
|
751
|
+
async invalidate(props) {
|
|
752
|
+
const previous = [];
|
|
753
|
+
if (props) {
|
|
754
|
+
const [key] = Object.keys(props);
|
|
755
|
+
const value = props[key];
|
|
756
|
+
for (const layer of this.state.layers) {
|
|
757
|
+
if (layer.props?.[key]) {
|
|
758
|
+
previous.push({
|
|
759
|
+
...layer,
|
|
760
|
+
props: {
|
|
761
|
+
...layer.props,
|
|
762
|
+
[key]: value
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
previous.push(layer);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
await this.render({ previous });
|
|
771
|
+
}
|
|
772
|
+
async go(url, options = {}) {
|
|
773
|
+
const result = await this.render({ url });
|
|
774
|
+
if (result.context.url.pathname !== url) {
|
|
775
|
+
this.pushState(result.context.url.pathname);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (options.replace) {
|
|
779
|
+
this.pushState(url);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
this.pushState(url);
|
|
783
|
+
}
|
|
784
|
+
async render(options = {}) {
|
|
785
|
+
const previous = options.previous ?? this.state.layers;
|
|
786
|
+
const url = options.url ?? this.url;
|
|
787
|
+
this.transitioning = { to: url };
|
|
788
|
+
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
789
|
+
previous,
|
|
790
|
+
state: this.state
|
|
791
|
+
});
|
|
792
|
+
if (result.redirect) return await this.render({ url: result.redirect });
|
|
793
|
+
this.transitioning = void 0;
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Get embedded layers from the server.
|
|
798
|
+
*/
|
|
799
|
+
getHydrationState() {
|
|
800
|
+
try {
|
|
801
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.error(error);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
ready = $hook({
|
|
807
|
+
on: "ready",
|
|
808
|
+
handler: async () => {
|
|
809
|
+
const hydration = this.getHydrationState();
|
|
810
|
+
const previous = hydration?.layers ?? [];
|
|
811
|
+
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
|
|
812
|
+
const { context } = await this.render({ previous });
|
|
813
|
+
await this.alepha.emit("react:browser:render", {
|
|
814
|
+
state: this.state,
|
|
815
|
+
context,
|
|
816
|
+
hydration
|
|
817
|
+
});
|
|
818
|
+
window.addEventListener("popstate", () => {
|
|
819
|
+
if (this.state.pathname === location.pathname) return;
|
|
820
|
+
this.render();
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
|
|
612
826
|
//#endregion
|
|
613
827
|
//#region src/providers/ReactServerProvider.ts
|
|
614
828
|
const envSchema = t.object({
|
|
@@ -624,18 +838,15 @@ var ReactServerProvider = class {
|
|
|
624
838
|
serverStaticProvider = $inject(ServerStaticProvider);
|
|
625
839
|
serverRouterProvider = $inject(ServerRouterProvider);
|
|
626
840
|
serverTimingProvider = $inject(ServerTimingProvider);
|
|
627
|
-
env = $
|
|
841
|
+
env = $env(envSchema);
|
|
628
842
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
629
843
|
onConfigure = $hook({
|
|
630
844
|
on: "configure",
|
|
631
845
|
handler: async () => {
|
|
632
|
-
const pages = this.alepha.
|
|
846
|
+
const pages = this.alepha.descriptors($page);
|
|
633
847
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
634
|
-
this.alepha.state("
|
|
635
|
-
for (const
|
|
636
|
-
const name = value[OPTIONS].name ?? key;
|
|
637
|
-
instance[key].render = this.createRenderFunction(name);
|
|
638
|
-
}
|
|
848
|
+
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
849
|
+
for (const page of pages) page.render = this.createRenderFunction(page.name);
|
|
639
850
|
if (this.alepha.isServerless() === "vite") {
|
|
640
851
|
await this.configureVite(ssrEnabled);
|
|
641
852
|
return;
|
|
@@ -655,7 +866,7 @@ var ReactServerProvider = class {
|
|
|
655
866
|
return;
|
|
656
867
|
}
|
|
657
868
|
this.log.info("SSR is disabled, use History API fallback");
|
|
658
|
-
|
|
869
|
+
this.serverRouterProvider.createRoute({
|
|
659
870
|
path: "*",
|
|
660
871
|
handler: async ({ url, reply }) => {
|
|
661
872
|
if (url.pathname.includes(".")) {
|
|
@@ -671,13 +882,13 @@ var ReactServerProvider = class {
|
|
|
671
882
|
}
|
|
672
883
|
});
|
|
673
884
|
get template() {
|
|
674
|
-
return this.alepha.state("
|
|
885
|
+
return this.alepha.state("react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
675
886
|
}
|
|
676
887
|
async registerPages(templateLoader) {
|
|
677
888
|
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
678
889
|
if (page.children?.length) continue;
|
|
679
890
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
680
|
-
|
|
891
|
+
this.serverRouterProvider.createRoute({
|
|
681
892
|
...page,
|
|
682
893
|
schema: void 0,
|
|
683
894
|
method: "GET",
|
|
@@ -692,7 +903,7 @@ var ReactServerProvider = class {
|
|
|
692
903
|
return "";
|
|
693
904
|
}
|
|
694
905
|
async configureStaticServer(root) {
|
|
695
|
-
await this.serverStaticProvider.
|
|
906
|
+
await this.serverStaticProvider.createStaticServer({
|
|
696
907
|
root,
|
|
697
908
|
path: this.env.REACT_SERVER_PREFIX
|
|
698
909
|
});
|
|
@@ -746,7 +957,7 @@ var ReactServerProvider = class {
|
|
|
746
957
|
onError: () => null
|
|
747
958
|
};
|
|
748
959
|
if (this.alepha.has(ServerLinksProvider)) {
|
|
749
|
-
const srv = this.alepha.
|
|
960
|
+
const srv = this.alepha.inject(ServerLinksProvider);
|
|
750
961
|
const schema = apiLinksResponseSchema;
|
|
751
962
|
context.links = this.alepha.parse(schema, await srv.getLinks({
|
|
752
963
|
user: serverRequest.user,
|
|
@@ -836,208 +1047,24 @@ var ReactServerProvider = class {
|
|
|
836
1047
|
}
|
|
837
1048
|
};
|
|
838
1049
|
|
|
839
|
-
//#endregion
|
|
840
|
-
//#region src/providers/BrowserRouterProvider.ts
|
|
841
|
-
var BrowserRouterProvider = class extends RouterProvider {
|
|
842
|
-
log = $logger();
|
|
843
|
-
alepha = $inject(Alepha);
|
|
844
|
-
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
845
|
-
add(entry) {
|
|
846
|
-
this.pageDescriptorProvider.add(entry);
|
|
847
|
-
}
|
|
848
|
-
configure = $hook({
|
|
849
|
-
on: "configure",
|
|
850
|
-
handler: async () => {
|
|
851
|
-
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
852
|
-
path: page.match,
|
|
853
|
-
page
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
async transition(url, options = {}) {
|
|
858
|
-
const { pathname, search } = url;
|
|
859
|
-
const state = {
|
|
860
|
-
pathname,
|
|
861
|
-
search,
|
|
862
|
-
layers: []
|
|
863
|
-
};
|
|
864
|
-
const context = {
|
|
865
|
-
url,
|
|
866
|
-
query: {},
|
|
867
|
-
params: {},
|
|
868
|
-
onError: () => null,
|
|
869
|
-
...options.context ?? {}
|
|
870
|
-
};
|
|
871
|
-
await this.alepha.emit("react:transition:begin", {
|
|
872
|
-
state,
|
|
873
|
-
context
|
|
874
|
-
});
|
|
875
|
-
try {
|
|
876
|
-
const previous = options.previous;
|
|
877
|
-
const { route, params } = this.match(pathname);
|
|
878
|
-
const query = {};
|
|
879
|
-
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
880
|
-
context.query = query;
|
|
881
|
-
context.params = params ?? {};
|
|
882
|
-
context.previous = previous;
|
|
883
|
-
if (isPageRoute(route)) {
|
|
884
|
-
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
885
|
-
if (result.redirect) return {
|
|
886
|
-
redirect: result.redirect,
|
|
887
|
-
state,
|
|
888
|
-
context
|
|
889
|
-
};
|
|
890
|
-
state.layers = result.layers;
|
|
891
|
-
}
|
|
892
|
-
if (state.layers.length === 0) state.layers.push({
|
|
893
|
-
name: "not-found",
|
|
894
|
-
element: createElement(NotFoundPage),
|
|
895
|
-
index: 0,
|
|
896
|
-
path: "/"
|
|
897
|
-
});
|
|
898
|
-
await this.alepha.emit("react:transition:success", {
|
|
899
|
-
state,
|
|
900
|
-
context
|
|
901
|
-
});
|
|
902
|
-
} catch (e) {
|
|
903
|
-
this.log.error(e);
|
|
904
|
-
state.layers = [{
|
|
905
|
-
name: "error",
|
|
906
|
-
element: this.pageDescriptorProvider.renderError(e),
|
|
907
|
-
index: 0,
|
|
908
|
-
path: "/"
|
|
909
|
-
}];
|
|
910
|
-
await this.alepha.emit("react:transition:error", {
|
|
911
|
-
error: e,
|
|
912
|
-
state,
|
|
913
|
-
context
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
if (options.state) {
|
|
917
|
-
options.state.layers = state.layers;
|
|
918
|
-
options.state.pathname = state.pathname;
|
|
919
|
-
options.state.search = state.search;
|
|
920
|
-
}
|
|
921
|
-
await this.alepha.emit("react:transition:end", {
|
|
922
|
-
state: options.state,
|
|
923
|
-
context
|
|
924
|
-
});
|
|
925
|
-
return {
|
|
926
|
-
context,
|
|
927
|
-
state
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
root(state, context) {
|
|
931
|
-
return this.pageDescriptorProvider.root(state, context);
|
|
932
|
-
}
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
//#endregion
|
|
936
|
-
//#region src/providers/ReactBrowserProvider.ts
|
|
937
|
-
var ReactBrowserProvider = class {
|
|
938
|
-
log = $logger();
|
|
939
|
-
client = $inject(LinkProvider);
|
|
940
|
-
alepha = $inject(Alepha);
|
|
941
|
-
router = $inject(BrowserRouterProvider);
|
|
942
|
-
root;
|
|
943
|
-
transitioning;
|
|
944
|
-
state = {
|
|
945
|
-
layers: [],
|
|
946
|
-
pathname: "",
|
|
947
|
-
search: ""
|
|
948
|
-
};
|
|
949
|
-
get document() {
|
|
950
|
-
return window.document;
|
|
951
|
-
}
|
|
952
|
-
get history() {
|
|
953
|
-
return window.history;
|
|
954
|
-
}
|
|
955
|
-
get url() {
|
|
956
|
-
return window.location.pathname + window.location.search;
|
|
957
|
-
}
|
|
958
|
-
async invalidate(props) {
|
|
959
|
-
const previous = [];
|
|
960
|
-
if (props) {
|
|
961
|
-
const [key] = Object.keys(props);
|
|
962
|
-
const value = props[key];
|
|
963
|
-
for (const layer of this.state.layers) {
|
|
964
|
-
if (layer.props?.[key]) {
|
|
965
|
-
previous.push({
|
|
966
|
-
...layer,
|
|
967
|
-
props: {
|
|
968
|
-
...layer.props,
|
|
969
|
-
[key]: value
|
|
970
|
-
}
|
|
971
|
-
});
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
974
|
-
previous.push(layer);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
await this.render({ previous });
|
|
978
|
-
}
|
|
979
|
-
async go(url, options = {}) {
|
|
980
|
-
const result = await this.render({ url });
|
|
981
|
-
if (result.context.url.pathname !== url) {
|
|
982
|
-
this.history.replaceState({}, "", result.context.url.pathname);
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if (options.replace) {
|
|
986
|
-
this.history.replaceState({}, "", url);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
this.history.pushState({}, "", url);
|
|
990
|
-
}
|
|
991
|
-
async render(options = {}) {
|
|
992
|
-
const previous = options.previous ?? this.state.layers;
|
|
993
|
-
const url = options.url ?? this.url;
|
|
994
|
-
this.transitioning = { to: url };
|
|
995
|
-
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
996
|
-
previous,
|
|
997
|
-
state: this.state
|
|
998
|
-
});
|
|
999
|
-
if (result.redirect) return await this.render({ url: result.redirect });
|
|
1000
|
-
this.transitioning = void 0;
|
|
1001
|
-
return result;
|
|
1002
|
-
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Get embedded layers from the server.
|
|
1005
|
-
*/
|
|
1006
|
-
getHydrationState() {
|
|
1007
|
-
try {
|
|
1008
|
-
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
console.error(error);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
ready = $hook({
|
|
1014
|
-
on: "ready",
|
|
1015
|
-
handler: async () => {
|
|
1016
|
-
const hydration = this.getHydrationState();
|
|
1017
|
-
const previous = hydration?.layers ?? [];
|
|
1018
|
-
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
|
|
1019
|
-
const { context } = await this.render({ previous });
|
|
1020
|
-
await this.alepha.emit("react:browser:render", {
|
|
1021
|
-
state: this.state,
|
|
1022
|
-
context,
|
|
1023
|
-
hydration
|
|
1024
|
-
});
|
|
1025
|
-
window.addEventListener("popstate", () => {
|
|
1026
|
-
this.render();
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
1050
|
//#endregion
|
|
1033
1051
|
//#region src/hooks/RouterHookApi.ts
|
|
1034
1052
|
var RouterHookApi = class {
|
|
1035
|
-
constructor(pages, state, layer, browser) {
|
|
1053
|
+
constructor(pages, context, state, layer, browser) {
|
|
1036
1054
|
this.pages = pages;
|
|
1055
|
+
this.context = context;
|
|
1037
1056
|
this.state = state;
|
|
1038
1057
|
this.layer = layer;
|
|
1039
1058
|
this.browser = browser;
|
|
1040
1059
|
}
|
|
1060
|
+
getURL() {
|
|
1061
|
+
if (!this.browser) return this.context.url;
|
|
1062
|
+
return new URL(this.location.href);
|
|
1063
|
+
}
|
|
1064
|
+
get location() {
|
|
1065
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
1066
|
+
return this.browser.location;
|
|
1067
|
+
}
|
|
1041
1068
|
get current() {
|
|
1042
1069
|
return this.state;
|
|
1043
1070
|
}
|
|
@@ -1113,9 +1140,9 @@ const useRouter = () => {
|
|
|
1113
1140
|
const layer = useContext(RouterLayerContext);
|
|
1114
1141
|
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1115
1142
|
const pages = useMemo(() => {
|
|
1116
|
-
return ctx.alepha.
|
|
1143
|
+
return ctx.alepha.inject(PageDescriptorProvider).getPages();
|
|
1117
1144
|
}, []);
|
|
1118
|
-
return useMemo(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.
|
|
1145
|
+
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
1119
1146
|
};
|
|
1120
1147
|
|
|
1121
1148
|
//#endregion
|
|
@@ -1123,11 +1150,11 @@ const useRouter = () => {
|
|
|
1123
1150
|
const Link = (props) => {
|
|
1124
1151
|
React.useContext(RouterContext);
|
|
1125
1152
|
const router = useRouter();
|
|
1126
|
-
const to = typeof props.to === "string" ? props.to : props.to
|
|
1153
|
+
const to = typeof props.to === "string" ? props.to : props.to.options.path;
|
|
1127
1154
|
if (!to) return null;
|
|
1128
|
-
const can = typeof props.to === "string" ? void 0 : props.to
|
|
1155
|
+
const can = typeof props.to === "string" ? void 0 : props.to.options.can;
|
|
1129
1156
|
if (can && !can()) return null;
|
|
1130
|
-
const name = typeof props.to === "string" ? void 0 : props.to
|
|
1157
|
+
const name = typeof props.to === "string" ? void 0 : props.to.options.name;
|
|
1131
1158
|
const anchorProps = {
|
|
1132
1159
|
...props,
|
|
1133
1160
|
to: void 0
|
|
@@ -1179,7 +1206,7 @@ const useActive = (path) => {
|
|
|
1179
1206
|
const useInject = (clazz) => {
|
|
1180
1207
|
const ctx = useContext(RouterContext);
|
|
1181
1208
|
if (!ctx) throw new Error("useRouter must be used within a <RouterProvider>");
|
|
1182
|
-
return useMemo(() => ctx.alepha.
|
|
1209
|
+
return useMemo(() => ctx.alepha.inject(clazz), []);
|
|
1183
1210
|
};
|
|
1184
1211
|
|
|
1185
1212
|
//#endregion
|
|
@@ -1235,20 +1262,26 @@ const useRouterState = () => {
|
|
|
1235
1262
|
//#endregion
|
|
1236
1263
|
//#region src/index.ts
|
|
1237
1264
|
/**
|
|
1238
|
-
*
|
|
1265
|
+
* Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
1239
1266
|
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1267
|
+
* The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
1268
|
+
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
1269
|
+
* type safety and schema validation for route parameters and data.
|
|
1242
1270
|
*
|
|
1243
1271
|
* @see {@link $page}
|
|
1244
1272
|
* @module alepha.react
|
|
1245
1273
|
*/
|
|
1246
|
-
|
|
1247
|
-
name
|
|
1248
|
-
$
|
|
1249
|
-
|
|
1250
|
-
|
|
1274
|
+
const AlephaReact = $module({
|
|
1275
|
+
name: "alepha.react",
|
|
1276
|
+
descriptors: [$page],
|
|
1277
|
+
services: [
|
|
1278
|
+
ReactServerProvider,
|
|
1279
|
+
PageDescriptorProvider,
|
|
1280
|
+
ReactBrowserProvider
|
|
1281
|
+
],
|
|
1282
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider)
|
|
1283
|
+
});
|
|
1251
1284
|
|
|
1252
1285
|
//#endregion
|
|
1253
|
-
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1286
|
+
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1254
1287
|
//# sourceMappingURL=index.js.map
|