@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/README.md
CHANGED
|
@@ -1 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Alepha React
|
|
2
|
+
|
|
3
|
+
Build server-side rendered (SSR) or single-page React applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This package is part of the Alepha framework and can be installed via the all-in-one package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install alepha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Alternatively, you can install it individually:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @alepha/core @alepha/react
|
|
17
|
+
```
|
|
18
|
+
## Module
|
|
19
|
+
|
|
20
|
+
Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
21
|
+
|
|
22
|
+
The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
23
|
+
It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
24
|
+
type safety and schema validation for route parameters and data.
|
|
25
|
+
|
|
26
|
+
## API Reference
|
|
27
|
+
|
|
28
|
+
### Descriptors
|
|
29
|
+
|
|
30
|
+
#### $page()
|
|
31
|
+
|
|
32
|
+
Main descriptor for defining a React route in the application.
|
package/dist/index.browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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 } from "@alepha/server";
|
|
3
3
|
import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
|
|
4
4
|
import { RouterProvider } from "@alepha/router";
|
|
@@ -7,21 +7,25 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
8
8
|
|
|
9
9
|
//#region src/descriptors/$page.ts
|
|
10
|
-
const KEY = "PAGE";
|
|
11
10
|
/**
|
|
12
11
|
* Main descriptor for defining a React route in the application.
|
|
13
12
|
*/
|
|
14
13
|
const $page = (options) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
return createDescriptor(PageDescriptor, options);
|
|
15
|
+
};
|
|
16
|
+
var PageDescriptor = class extends Descriptor {
|
|
17
|
+
get name() {
|
|
18
|
+
return this.options.name ?? this.config.propertyKey;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
22
|
+
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
23
|
+
*/
|
|
24
|
+
async render(options) {
|
|
25
|
+
throw new NotImplementedError("");
|
|
26
|
+
}
|
|
23
27
|
};
|
|
24
|
-
$page[KIND] =
|
|
28
|
+
$page[KIND] = PageDescriptor;
|
|
25
29
|
|
|
26
30
|
//#endregion
|
|
27
31
|
//#region src/components/NotFound.tsx
|
|
@@ -311,7 +315,7 @@ const NestedView = (props) => {
|
|
|
311
315
|
const index = layer?.index ?? 0;
|
|
312
316
|
const [view, setView] = useState(app?.state.layers[index]?.element);
|
|
313
317
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
314
|
-
setView(state.layers[index]?.element);
|
|
318
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
315
319
|
} }, [app]);
|
|
316
320
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
317
321
|
const element = view ?? props.children ?? null;
|
|
@@ -337,7 +341,7 @@ var RedirectionError = class extends Error {
|
|
|
337
341
|
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
338
342
|
var PageDescriptorProvider = class {
|
|
339
343
|
log = $logger();
|
|
340
|
-
env = $
|
|
344
|
+
env = $env(envSchema$1);
|
|
341
345
|
alepha = $inject(Alepha);
|
|
342
346
|
pages = [];
|
|
343
347
|
getPages() {
|
|
@@ -385,19 +389,18 @@ var PageDescriptorProvider = class {
|
|
|
385
389
|
const route$1 = it.route;
|
|
386
390
|
const config = {};
|
|
387
391
|
try {
|
|
388
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) :
|
|
392
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
|
|
389
393
|
} catch (e) {
|
|
390
394
|
it.error = e;
|
|
391
395
|
break;
|
|
392
396
|
}
|
|
393
397
|
try {
|
|
394
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) :
|
|
398
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
|
|
395
399
|
} catch (e) {
|
|
396
400
|
it.error = e;
|
|
397
401
|
break;
|
|
398
402
|
}
|
|
399
403
|
it.config = { ...config };
|
|
400
|
-
if (!route$1.resolve) continue;
|
|
401
404
|
const previous = request.previous;
|
|
402
405
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
403
406
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -412,6 +415,7 @@ var PageDescriptorProvider = class {
|
|
|
412
415
|
if (prev === curr) {
|
|
413
416
|
it.props = previous[i].props;
|
|
414
417
|
it.error = previous[i].error;
|
|
418
|
+
it.cache = true;
|
|
415
419
|
context = {
|
|
416
420
|
...context,
|
|
417
421
|
...it.props
|
|
@@ -420,6 +424,7 @@ var PageDescriptorProvider = class {
|
|
|
420
424
|
}
|
|
421
425
|
forceRefresh = true;
|
|
422
426
|
}
|
|
427
|
+
if (!route$1.resolve) continue;
|
|
423
428
|
try {
|
|
424
429
|
const props = await route$1.resolve?.({
|
|
425
430
|
...request,
|
|
@@ -466,7 +471,7 @@ var PageDescriptorProvider = class {
|
|
|
466
471
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
467
472
|
index: i + 1,
|
|
468
473
|
path,
|
|
469
|
-
route
|
|
474
|
+
route: it.route
|
|
470
475
|
});
|
|
471
476
|
break;
|
|
472
477
|
}
|
|
@@ -482,7 +487,8 @@ var PageDescriptorProvider = class {
|
|
|
482
487
|
element: this.renderView(i + 1, path, element, it.route),
|
|
483
488
|
index: i + 1,
|
|
484
489
|
path,
|
|
485
|
-
route
|
|
490
|
+
route: it.route,
|
|
491
|
+
cache: it.cache
|
|
486
492
|
});
|
|
487
493
|
}
|
|
488
494
|
return {
|
|
@@ -541,18 +547,17 @@ var PageDescriptorProvider = class {
|
|
|
541
547
|
on: "configure",
|
|
542
548
|
handler: () => {
|
|
543
549
|
let hasNotFoundHandler = false;
|
|
544
|
-
const pages = this.alepha.
|
|
550
|
+
const pages = this.alepha.descriptors($page);
|
|
545
551
|
const hasParent = (it) => {
|
|
546
552
|
for (const page of pages) {
|
|
547
|
-
const children = page.
|
|
553
|
+
const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
|
|
548
554
|
if (children.includes(it)) return true;
|
|
549
555
|
}
|
|
550
556
|
};
|
|
551
|
-
for (const
|
|
552
|
-
|
|
553
|
-
if (hasParent(
|
|
554
|
-
|
|
555
|
-
this.add(this.map(pages, value));
|
|
557
|
+
for (const page of pages) {
|
|
558
|
+
if (page.options.path === "/*") hasNotFoundHandler = true;
|
|
559
|
+
if (hasParent(page)) continue;
|
|
560
|
+
this.add(this.map(pages, page));
|
|
556
561
|
}
|
|
557
562
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
558
563
|
path: "/*",
|
|
@@ -566,9 +571,10 @@ var PageDescriptorProvider = class {
|
|
|
566
571
|
}
|
|
567
572
|
});
|
|
568
573
|
map(pages, target) {
|
|
569
|
-
const children = target
|
|
574
|
+
const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
|
|
570
575
|
return {
|
|
571
|
-
...target
|
|
576
|
+
...target.options,
|
|
577
|
+
name: target.name,
|
|
572
578
|
parent: void 0,
|
|
573
579
|
children: children.map((it) => this.map(pages, it))
|
|
574
580
|
};
|
|
@@ -721,8 +727,22 @@ var ReactBrowserProvider = class {
|
|
|
721
727
|
get history() {
|
|
722
728
|
return window.history;
|
|
723
729
|
}
|
|
730
|
+
get location() {
|
|
731
|
+
return window.location;
|
|
732
|
+
}
|
|
724
733
|
get url() {
|
|
725
|
-
|
|
734
|
+
let url = this.location.pathname + this.location.search;
|
|
735
|
+
if (import.meta?.env?.BASE_URL) {
|
|
736
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
737
|
+
if (!url.startsWith("/")) url = `/${url}`;
|
|
738
|
+
}
|
|
739
|
+
return url;
|
|
740
|
+
}
|
|
741
|
+
pushState(url, replace) {
|
|
742
|
+
let path = url;
|
|
743
|
+
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
744
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
745
|
+
else this.history.pushState({}, "", path);
|
|
726
746
|
}
|
|
727
747
|
async invalidate(props) {
|
|
728
748
|
const previous = [];
|
|
@@ -748,14 +768,14 @@ var ReactBrowserProvider = class {
|
|
|
748
768
|
async go(url, options = {}) {
|
|
749
769
|
const result = await this.render({ url });
|
|
750
770
|
if (result.context.url.pathname !== url) {
|
|
751
|
-
this.
|
|
771
|
+
this.pushState(result.context.url.pathname);
|
|
752
772
|
return;
|
|
753
773
|
}
|
|
754
774
|
if (options.replace) {
|
|
755
|
-
this.
|
|
775
|
+
this.pushState(url);
|
|
756
776
|
return;
|
|
757
777
|
}
|
|
758
|
-
this.
|
|
778
|
+
this.pushState(url);
|
|
759
779
|
}
|
|
760
780
|
async render(options = {}) {
|
|
761
781
|
const previous = options.previous ?? this.state.layers;
|
|
@@ -792,6 +812,7 @@ var ReactBrowserProvider = class {
|
|
|
792
812
|
hydration
|
|
793
813
|
});
|
|
794
814
|
window.addEventListener("popstate", () => {
|
|
815
|
+
if (this.state.pathname === location.pathname) return;
|
|
795
816
|
this.render();
|
|
796
817
|
});
|
|
797
818
|
}
|
|
@@ -804,9 +825,10 @@ const envSchema = t.object({ REACT_ROOT_ID: t.string({ default: "root" }) });
|
|
|
804
825
|
var ReactBrowserRenderer = class {
|
|
805
826
|
browserProvider = $inject(ReactBrowserProvider);
|
|
806
827
|
browserRouterProvider = $inject(BrowserRouterProvider);
|
|
807
|
-
env = $
|
|
828
|
+
env = $env(envSchema);
|
|
808
829
|
log = $logger();
|
|
809
830
|
root;
|
|
831
|
+
options = { scrollRestoration: "top" };
|
|
810
832
|
getRootElement() {
|
|
811
833
|
const root = this.browserProvider.document.getElementById(this.env.REACT_ROOT_ID);
|
|
812
834
|
if (root) return root;
|
|
@@ -829,17 +851,32 @@ var ReactBrowserRenderer = class {
|
|
|
829
851
|
}
|
|
830
852
|
}
|
|
831
853
|
});
|
|
854
|
+
onTransitionEnd = $hook({
|
|
855
|
+
on: "react:transition:end",
|
|
856
|
+
handler: () => {
|
|
857
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") window.scrollTo(0, 0);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
832
860
|
};
|
|
833
861
|
|
|
834
862
|
//#endregion
|
|
835
863
|
//#region src/hooks/RouterHookApi.ts
|
|
836
864
|
var RouterHookApi = class {
|
|
837
|
-
constructor(pages, state, layer, browser) {
|
|
865
|
+
constructor(pages, context, state, layer, browser) {
|
|
838
866
|
this.pages = pages;
|
|
867
|
+
this.context = context;
|
|
839
868
|
this.state = state;
|
|
840
869
|
this.layer = layer;
|
|
841
870
|
this.browser = browser;
|
|
842
871
|
}
|
|
872
|
+
getURL() {
|
|
873
|
+
if (!this.browser) return this.context.url;
|
|
874
|
+
return new URL(this.location.href);
|
|
875
|
+
}
|
|
876
|
+
get location() {
|
|
877
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
878
|
+
return this.browser.location;
|
|
879
|
+
}
|
|
843
880
|
get current() {
|
|
844
881
|
return this.state;
|
|
845
882
|
}
|
|
@@ -915,9 +952,9 @@ const useRouter = () => {
|
|
|
915
952
|
const layer = useContext(RouterLayerContext);
|
|
916
953
|
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
917
954
|
const pages = useMemo(() => {
|
|
918
|
-
return ctx.alepha.
|
|
955
|
+
return ctx.alepha.inject(PageDescriptorProvider).getPages();
|
|
919
956
|
}, []);
|
|
920
|
-
return useMemo(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.
|
|
957
|
+
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
921
958
|
};
|
|
922
959
|
|
|
923
960
|
//#endregion
|
|
@@ -925,11 +962,11 @@ const useRouter = () => {
|
|
|
925
962
|
const Link = (props) => {
|
|
926
963
|
React.useContext(RouterContext);
|
|
927
964
|
const router = useRouter();
|
|
928
|
-
const to = typeof props.to === "string" ? props.to : props.to
|
|
965
|
+
const to = typeof props.to === "string" ? props.to : props.to.options.path;
|
|
929
966
|
if (!to) return null;
|
|
930
|
-
const can = typeof props.to === "string" ? void 0 : props.to
|
|
967
|
+
const can = typeof props.to === "string" ? void 0 : props.to.options.can;
|
|
931
968
|
if (can && !can()) return null;
|
|
932
|
-
const name = typeof props.to === "string" ? void 0 : props.to
|
|
969
|
+
const name = typeof props.to === "string" ? void 0 : props.to.options.name;
|
|
933
970
|
const anchorProps = {
|
|
934
971
|
...props,
|
|
935
972
|
to: void 0
|
|
@@ -981,7 +1018,7 @@ const useActive = (path) => {
|
|
|
981
1018
|
const useInject = (clazz) => {
|
|
982
1019
|
const ctx = useContext(RouterContext);
|
|
983
1020
|
if (!ctx) throw new Error("useRouter must be used within a <RouterProvider>");
|
|
984
|
-
return useMemo(() => ctx.alepha.
|
|
1021
|
+
return useMemo(() => ctx.alepha.inject(clazz), []);
|
|
985
1022
|
};
|
|
986
1023
|
|
|
987
1024
|
//#endregion
|
|
@@ -1036,12 +1073,18 @@ const useRouterState = () => {
|
|
|
1036
1073
|
|
|
1037
1074
|
//#endregion
|
|
1038
1075
|
//#region src/index.browser.ts
|
|
1039
|
-
|
|
1040
|
-
name
|
|
1041
|
-
$
|
|
1042
|
-
|
|
1043
|
-
|
|
1076
|
+
const AlephaReact = $module({
|
|
1077
|
+
name: "alepha.react",
|
|
1078
|
+
descriptors: [$page],
|
|
1079
|
+
services: [
|
|
1080
|
+
PageDescriptorProvider,
|
|
1081
|
+
ReactBrowserRenderer,
|
|
1082
|
+
BrowserRouterProvider,
|
|
1083
|
+
ReactBrowserProvider
|
|
1084
|
+
],
|
|
1085
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider).with(ReactBrowserRenderer)
|
|
1086
|
+
});
|
|
1044
1087
|
|
|
1045
1088
|
//#endregion
|
|
1046
|
-
export { $page, AlephaReact, BrowserRouterProvider, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, PageDescriptorProvider, ReactBrowserProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1089
|
+
export { $page, AlephaReact, BrowserRouterProvider, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1047
1090
|
//# sourceMappingURL=index.browser.js.map
|