@alepha/react 0.8.0 → 0.8.1
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 +153 -1
- package/dist/index.browser.js +47 -14
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +162 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -29
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +157 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -19
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +40 -40
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useRouter.ts +1 -0
- package/src/index.shared.ts +1 -0
- package/src/index.ts +125 -3
- package/src/providers/PageDescriptorProvider.ts +18 -13
- package/src/providers/ReactBrowserProvider.ts +37 -4
- package/src/providers/ReactBrowserRenderer.ts +20 -0
- package/src/providers/ReactServerProvider.ts +4 -4
package/README.md
CHANGED
|
@@ -1 +1,153 @@
|
|
|
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
|
+
**Key Features:**
|
|
27
|
+
- Declarative page definition with `$page` descriptor
|
|
28
|
+
- Server-side rendering (SSR) with automatic hydration
|
|
29
|
+
- Type-safe routing with parameter validation
|
|
30
|
+
- Schema-based data resolution and validation
|
|
31
|
+
- SEO-friendly meta tag management
|
|
32
|
+
- Automatic code splitting and lazy loading
|
|
33
|
+
- Client-side navigation with browser history
|
|
34
|
+
|
|
35
|
+
**Basic Usage:**
|
|
36
|
+
```ts
|
|
37
|
+
import { Alepha, run, t } from "alepha";
|
|
38
|
+
import { AlephaReact, $page } from "alepha/react";
|
|
39
|
+
|
|
40
|
+
class AppRoutes {
|
|
41
|
+
// Home page
|
|
42
|
+
home = $page({
|
|
43
|
+
path: "/",
|
|
44
|
+
component: () => (
|
|
45
|
+
<div>
|
|
46
|
+
<h1>Welcome to Alepha</h1>
|
|
47
|
+
<p>Build amazing React applications!</p>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// About page with meta tags
|
|
53
|
+
about = $page({
|
|
54
|
+
path: "/about",
|
|
55
|
+
head: {
|
|
56
|
+
title: "About Us",
|
|
57
|
+
description: "Learn more about our mission",
|
|
58
|
+
},
|
|
59
|
+
component: () => (
|
|
60
|
+
<div>
|
|
61
|
+
<h1>About Us</h1>
|
|
62
|
+
<p>Learn more about our mission.</p>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const alepha = Alepha.create()
|
|
69
|
+
.with(AlephaReact)
|
|
70
|
+
.with(AppRoutes);
|
|
71
|
+
|
|
72
|
+
run(alepha);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Dynamic Routes with Parameters:**
|
|
76
|
+
```tsx
|
|
77
|
+
class UserRoutes {
|
|
78
|
+
userProfile = $page({
|
|
79
|
+
path: "/users/:id",
|
|
80
|
+
schema: {
|
|
81
|
+
params: t.object({
|
|
82
|
+
id: t.string(),
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
resolve: async ({ params }) => {
|
|
86
|
+
// Fetch user data server-side
|
|
87
|
+
const user = await getUserById(params.id);
|
|
88
|
+
return { user };
|
|
89
|
+
},
|
|
90
|
+
head: ({ user }) => ({
|
|
91
|
+
title: `${user.name} - Profile`,
|
|
92
|
+
description: `View ${user.name}'s profile`,
|
|
93
|
+
}),
|
|
94
|
+
component: ({ user }) => (
|
|
95
|
+
<div>
|
|
96
|
+
<h1>{user.name}</h1>
|
|
97
|
+
<p>Email: {user.email}</p>
|
|
98
|
+
</div>
|
|
99
|
+
),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
userSettings = $page({
|
|
103
|
+
path: "/users/:id/settings",
|
|
104
|
+
schema: {
|
|
105
|
+
params: t.object({
|
|
106
|
+
id: t.string(),
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
component: ({ params }) => (
|
|
110
|
+
<UserSettings userId={params.id} />
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Static Generation:**
|
|
117
|
+
```tsx
|
|
118
|
+
class BlogRoutes {
|
|
119
|
+
blogPost = $page({
|
|
120
|
+
path: "/blog/:slug",
|
|
121
|
+
schema: {
|
|
122
|
+
params: t.object({
|
|
123
|
+
slug: t.string(),
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
static: {
|
|
127
|
+
entries: [
|
|
128
|
+
{ params: { slug: "getting-started" } },
|
|
129
|
+
{ params: { slug: "advanced-features" } },
|
|
130
|
+
{ params: { slug: "deployment" } },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
resolve: ({ params }) => {
|
|
134
|
+
const post = getBlogPost(params.slug);
|
|
135
|
+
return { post };
|
|
136
|
+
},
|
|
137
|
+
component: ({ post }) => (
|
|
138
|
+
<article>
|
|
139
|
+
<h1>{post.title}</h1>
|
|
140
|
+
<div>{post.content}</div>
|
|
141
|
+
</article>
|
|
142
|
+
),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API Reference
|
|
148
|
+
|
|
149
|
+
### Descriptors
|
|
150
|
+
|
|
151
|
+
#### $page()
|
|
152
|
+
|
|
153
|
+
Main descriptor for defining a React route in the application.
|
package/dist/index.browser.js
CHANGED
|
@@ -311,7 +311,7 @@ const NestedView = (props) => {
|
|
|
311
311
|
const index = layer?.index ?? 0;
|
|
312
312
|
const [view, setView] = useState(app?.state.layers[index]?.element);
|
|
313
313
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
314
|
-
setView(state.layers[index]?.element);
|
|
314
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
315
315
|
} }, [app]);
|
|
316
316
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
317
317
|
const element = view ?? props.children ?? null;
|
|
@@ -385,19 +385,18 @@ var PageDescriptorProvider = class {
|
|
|
385
385
|
const route$1 = it.route;
|
|
386
386
|
const config = {};
|
|
387
387
|
try {
|
|
388
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) :
|
|
388
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
|
|
389
389
|
} catch (e) {
|
|
390
390
|
it.error = e;
|
|
391
391
|
break;
|
|
392
392
|
}
|
|
393
393
|
try {
|
|
394
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) :
|
|
394
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
|
|
395
395
|
} catch (e) {
|
|
396
396
|
it.error = e;
|
|
397
397
|
break;
|
|
398
398
|
}
|
|
399
399
|
it.config = { ...config };
|
|
400
|
-
if (!route$1.resolve) continue;
|
|
401
400
|
const previous = request.previous;
|
|
402
401
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
403
402
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -412,6 +411,7 @@ var PageDescriptorProvider = class {
|
|
|
412
411
|
if (prev === curr) {
|
|
413
412
|
it.props = previous[i].props;
|
|
414
413
|
it.error = previous[i].error;
|
|
414
|
+
it.cache = true;
|
|
415
415
|
context = {
|
|
416
416
|
...context,
|
|
417
417
|
...it.props
|
|
@@ -420,6 +420,7 @@ var PageDescriptorProvider = class {
|
|
|
420
420
|
}
|
|
421
421
|
forceRefresh = true;
|
|
422
422
|
}
|
|
423
|
+
if (!route$1.resolve) continue;
|
|
423
424
|
try {
|
|
424
425
|
const props = await route$1.resolve?.({
|
|
425
426
|
...request,
|
|
@@ -466,7 +467,7 @@ var PageDescriptorProvider = class {
|
|
|
466
467
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
467
468
|
index: i + 1,
|
|
468
469
|
path,
|
|
469
|
-
route
|
|
470
|
+
route: it.route
|
|
470
471
|
});
|
|
471
472
|
break;
|
|
472
473
|
}
|
|
@@ -482,7 +483,8 @@ var PageDescriptorProvider = class {
|
|
|
482
483
|
element: this.renderView(i + 1, path, element, it.route),
|
|
483
484
|
index: i + 1,
|
|
484
485
|
path,
|
|
485
|
-
route
|
|
486
|
+
route: it.route,
|
|
487
|
+
cache: it.cache
|
|
486
488
|
});
|
|
487
489
|
}
|
|
488
490
|
return {
|
|
@@ -550,8 +552,8 @@ var PageDescriptorProvider = class {
|
|
|
550
552
|
};
|
|
551
553
|
for (const { value, key } of pages) value[OPTIONS].name ??= key;
|
|
552
554
|
for (const { value } of pages) {
|
|
553
|
-
if (hasParent(value)) continue;
|
|
554
555
|
if (value[OPTIONS].path === "/*") hasNotFoundHandler = true;
|
|
556
|
+
if (hasParent(value)) continue;
|
|
555
557
|
this.add(this.map(pages, value));
|
|
556
558
|
}
|
|
557
559
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
@@ -721,8 +723,22 @@ var ReactBrowserProvider = class {
|
|
|
721
723
|
get history() {
|
|
722
724
|
return window.history;
|
|
723
725
|
}
|
|
726
|
+
get location() {
|
|
727
|
+
return window.location;
|
|
728
|
+
}
|
|
724
729
|
get url() {
|
|
725
|
-
|
|
730
|
+
let url = this.location.pathname + this.location.search;
|
|
731
|
+
if (import.meta?.env?.BASE_URL) {
|
|
732
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
733
|
+
if (!url.startsWith("/")) url = `/${url}`;
|
|
734
|
+
}
|
|
735
|
+
return url;
|
|
736
|
+
}
|
|
737
|
+
pushState(url, replace) {
|
|
738
|
+
let path = url;
|
|
739
|
+
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
740
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
741
|
+
else this.history.pushState({}, "", path);
|
|
726
742
|
}
|
|
727
743
|
async invalidate(props) {
|
|
728
744
|
const previous = [];
|
|
@@ -748,14 +764,14 @@ var ReactBrowserProvider = class {
|
|
|
748
764
|
async go(url, options = {}) {
|
|
749
765
|
const result = await this.render({ url });
|
|
750
766
|
if (result.context.url.pathname !== url) {
|
|
751
|
-
this.
|
|
767
|
+
this.pushState(result.context.url.pathname);
|
|
752
768
|
return;
|
|
753
769
|
}
|
|
754
770
|
if (options.replace) {
|
|
755
|
-
this.
|
|
771
|
+
this.pushState(url);
|
|
756
772
|
return;
|
|
757
773
|
}
|
|
758
|
-
this.
|
|
774
|
+
this.pushState(url);
|
|
759
775
|
}
|
|
760
776
|
async render(options = {}) {
|
|
761
777
|
const previous = options.previous ?? this.state.layers;
|
|
@@ -792,6 +808,7 @@ var ReactBrowserProvider = class {
|
|
|
792
808
|
hydration
|
|
793
809
|
});
|
|
794
810
|
window.addEventListener("popstate", () => {
|
|
811
|
+
if (this.state.pathname === location.pathname) return;
|
|
795
812
|
this.render();
|
|
796
813
|
});
|
|
797
814
|
}
|
|
@@ -807,6 +824,7 @@ var ReactBrowserRenderer = class {
|
|
|
807
824
|
env = $inject(envSchema);
|
|
808
825
|
log = $logger();
|
|
809
826
|
root;
|
|
827
|
+
options = { scrollRestoration: "top" };
|
|
810
828
|
getRootElement() {
|
|
811
829
|
const root = this.browserProvider.document.getElementById(this.env.REACT_ROOT_ID);
|
|
812
830
|
if (root) return root;
|
|
@@ -829,17 +847,32 @@ var ReactBrowserRenderer = class {
|
|
|
829
847
|
}
|
|
830
848
|
}
|
|
831
849
|
});
|
|
850
|
+
onTransitionEnd = $hook({
|
|
851
|
+
on: "react:transition:end",
|
|
852
|
+
handler: () => {
|
|
853
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") window.scrollTo(0, 0);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
832
856
|
};
|
|
833
857
|
|
|
834
858
|
//#endregion
|
|
835
859
|
//#region src/hooks/RouterHookApi.ts
|
|
836
860
|
var RouterHookApi = class {
|
|
837
|
-
constructor(pages, state, layer, browser) {
|
|
861
|
+
constructor(pages, context, state, layer, browser) {
|
|
838
862
|
this.pages = pages;
|
|
863
|
+
this.context = context;
|
|
839
864
|
this.state = state;
|
|
840
865
|
this.layer = layer;
|
|
841
866
|
this.browser = browser;
|
|
842
867
|
}
|
|
868
|
+
getURL() {
|
|
869
|
+
if (!this.browser) return this.context.url;
|
|
870
|
+
return new URL(this.location.href);
|
|
871
|
+
}
|
|
872
|
+
get location() {
|
|
873
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
874
|
+
return this.browser.location;
|
|
875
|
+
}
|
|
843
876
|
get current() {
|
|
844
877
|
return this.state;
|
|
845
878
|
}
|
|
@@ -917,7 +950,7 @@ const useRouter = () => {
|
|
|
917
950
|
const pages = useMemo(() => {
|
|
918
951
|
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
919
952
|
}, []);
|
|
920
|
-
return useMemo(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
953
|
+
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
921
954
|
};
|
|
922
955
|
|
|
923
956
|
//#endregion
|
|
@@ -1043,5 +1076,5 @@ var AlephaReact = class {
|
|
|
1043
1076
|
__bind($page, AlephaReact);
|
|
1044
1077
|
|
|
1045
1078
|
//#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 };
|
|
1079
|
+
export { $page, AlephaReact, BrowserRouterProvider, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptorProvider, ReactBrowserProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1047
1080
|
//# sourceMappingURL=index.browser.js.map
|