@alepha/react 0.7.7 → 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 +61 -26
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +175 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -41
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +197 -40
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +180 -31
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +50 -42
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useRouter.ts +1 -0
- package/src/index.browser.ts +4 -0
- package/src/index.shared.ts +1 -0
- package/src/index.ts +127 -3
- package/src/providers/BrowserRouterProvider.ts +1 -1
- package/src/providers/PageDescriptorProvider.ts +39 -23
- package/src/providers/ReactBrowserProvider.ts +38 -5
- package/src/providers/ReactBrowserRenderer.ts +21 -1
- package/src/providers/ReactServerProvider.ts +5 -5
- package/dist/index.browser.d.ts +0 -523
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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { $hook, $inject, $logger, Alepha, KIND, NotImplementedError, OPTIONS, __bind, __descriptor, t } from "@alepha/core";
|
|
2
|
+
import { AlephaServer } from "@alepha/server";
|
|
3
|
+
import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
|
|
2
4
|
import { RouterProvider } from "@alepha/router";
|
|
3
5
|
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
4
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { LinkProvider } from "@alepha/server-links";
|
|
6
7
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
7
8
|
|
|
8
9
|
//#region src/descriptors/$page.ts
|
|
@@ -12,11 +13,6 @@ const KEY = "PAGE";
|
|
|
12
13
|
*/
|
|
13
14
|
const $page = (options) => {
|
|
14
15
|
__descriptor(KEY);
|
|
15
|
-
if (options.children) for (const child of options.children) child[OPTIONS].parent = { [OPTIONS]: options };
|
|
16
|
-
if (options.parent) {
|
|
17
|
-
options.parent[OPTIONS].children ??= [];
|
|
18
|
-
options.parent[OPTIONS].children.push({ [OPTIONS]: options });
|
|
19
|
-
}
|
|
20
16
|
return {
|
|
21
17
|
[KIND]: KEY,
|
|
22
18
|
[OPTIONS]: options,
|
|
@@ -315,7 +311,7 @@ const NestedView = (props) => {
|
|
|
315
311
|
const index = layer?.index ?? 0;
|
|
316
312
|
const [view, setView] = useState(app?.state.layers[index]?.element);
|
|
317
313
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
318
|
-
setView(state.layers[index]?.element);
|
|
314
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
319
315
|
} }, [app]);
|
|
320
316
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
321
317
|
const element = view ?? props.children ?? null;
|
|
@@ -389,19 +385,18 @@ var PageDescriptorProvider = class {
|
|
|
389
385
|
const route$1 = it.route;
|
|
390
386
|
const config = {};
|
|
391
387
|
try {
|
|
392
|
-
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) : {};
|
|
393
389
|
} catch (e) {
|
|
394
390
|
it.error = e;
|
|
395
391
|
break;
|
|
396
392
|
}
|
|
397
393
|
try {
|
|
398
|
-
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) : {};
|
|
399
395
|
} catch (e) {
|
|
400
396
|
it.error = e;
|
|
401
397
|
break;
|
|
402
398
|
}
|
|
403
399
|
it.config = { ...config };
|
|
404
|
-
if (!route$1.resolve) continue;
|
|
405
400
|
const previous = request.previous;
|
|
406
401
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
407
402
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -416,6 +411,7 @@ var PageDescriptorProvider = class {
|
|
|
416
411
|
if (prev === curr) {
|
|
417
412
|
it.props = previous[i].props;
|
|
418
413
|
it.error = previous[i].error;
|
|
414
|
+
it.cache = true;
|
|
419
415
|
context = {
|
|
420
416
|
...context,
|
|
421
417
|
...it.props
|
|
@@ -424,6 +420,7 @@ var PageDescriptorProvider = class {
|
|
|
424
420
|
}
|
|
425
421
|
forceRefresh = true;
|
|
426
422
|
}
|
|
423
|
+
if (!route$1.resolve) continue;
|
|
427
424
|
try {
|
|
428
425
|
const props = await route$1.resolve?.({
|
|
429
426
|
...request,
|
|
@@ -470,7 +467,7 @@ var PageDescriptorProvider = class {
|
|
|
470
467
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
471
468
|
index: i + 1,
|
|
472
469
|
path,
|
|
473
|
-
route
|
|
470
|
+
route: it.route
|
|
474
471
|
});
|
|
475
472
|
break;
|
|
476
473
|
}
|
|
@@ -486,7 +483,8 @@ var PageDescriptorProvider = class {
|
|
|
486
483
|
element: this.renderView(i + 1, path, element, it.route),
|
|
487
484
|
index: i + 1,
|
|
488
485
|
path,
|
|
489
|
-
route
|
|
486
|
+
route: it.route,
|
|
487
|
+
cache: it.cache
|
|
490
488
|
});
|
|
491
489
|
}
|
|
492
490
|
return {
|
|
@@ -542,14 +540,20 @@ var PageDescriptorProvider = class {
|
|
|
542
540
|
} }, element);
|
|
543
541
|
}
|
|
544
542
|
configure = $hook({
|
|
545
|
-
|
|
543
|
+
on: "configure",
|
|
546
544
|
handler: () => {
|
|
547
545
|
let hasNotFoundHandler = false;
|
|
548
546
|
const pages = this.alepha.getDescriptorValues($page);
|
|
547
|
+
const hasParent = (it) => {
|
|
548
|
+
for (const page of pages) {
|
|
549
|
+
const children = page.value[OPTIONS].children ? Array.isArray(page.value[OPTIONS].children) ? page.value[OPTIONS].children : page.value[OPTIONS].children() : [];
|
|
550
|
+
if (children.includes(it)) return true;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
549
553
|
for (const { value, key } of pages) value[OPTIONS].name ??= key;
|
|
550
554
|
for (const { value } of pages) {
|
|
551
|
-
if (value[OPTIONS].parent) continue;
|
|
552
555
|
if (value[OPTIONS].path === "/*") hasNotFoundHandler = true;
|
|
556
|
+
if (hasParent(value)) continue;
|
|
553
557
|
this.add(this.map(pages, value));
|
|
554
558
|
}
|
|
555
559
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
@@ -564,7 +568,7 @@ var PageDescriptorProvider = class {
|
|
|
564
568
|
}
|
|
565
569
|
});
|
|
566
570
|
map(pages, target) {
|
|
567
|
-
const children = target[OPTIONS].children
|
|
571
|
+
const children = target[OPTIONS].children ? Array.isArray(target[OPTIONS].children) ? target[OPTIONS].children : target[OPTIONS].children() : [];
|
|
568
572
|
return {
|
|
569
573
|
...target[OPTIONS],
|
|
570
574
|
parent: void 0,
|
|
@@ -613,7 +617,7 @@ var BrowserRouterProvider = class extends RouterProvider {
|
|
|
613
617
|
this.pageDescriptorProvider.add(entry);
|
|
614
618
|
}
|
|
615
619
|
configure = $hook({
|
|
616
|
-
|
|
620
|
+
on: "configure",
|
|
617
621
|
handler: async () => {
|
|
618
622
|
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
619
623
|
path: page.match,
|
|
@@ -719,8 +723,22 @@ var ReactBrowserProvider = class {
|
|
|
719
723
|
get history() {
|
|
720
724
|
return window.history;
|
|
721
725
|
}
|
|
726
|
+
get location() {
|
|
727
|
+
return window.location;
|
|
728
|
+
}
|
|
722
729
|
get url() {
|
|
723
|
-
|
|
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);
|
|
724
742
|
}
|
|
725
743
|
async invalidate(props) {
|
|
726
744
|
const previous = [];
|
|
@@ -746,14 +764,14 @@ var ReactBrowserProvider = class {
|
|
|
746
764
|
async go(url, options = {}) {
|
|
747
765
|
const result = await this.render({ url });
|
|
748
766
|
if (result.context.url.pathname !== url) {
|
|
749
|
-
this.
|
|
767
|
+
this.pushState(result.context.url.pathname);
|
|
750
768
|
return;
|
|
751
769
|
}
|
|
752
770
|
if (options.replace) {
|
|
753
|
-
this.
|
|
771
|
+
this.pushState(url);
|
|
754
772
|
return;
|
|
755
773
|
}
|
|
756
|
-
this.
|
|
774
|
+
this.pushState(url);
|
|
757
775
|
}
|
|
758
776
|
async render(options = {}) {
|
|
759
777
|
const previous = options.previous ?? this.state.layers;
|
|
@@ -778,7 +796,7 @@ var ReactBrowserProvider = class {
|
|
|
778
796
|
}
|
|
779
797
|
}
|
|
780
798
|
ready = $hook({
|
|
781
|
-
|
|
799
|
+
on: "ready",
|
|
782
800
|
handler: async () => {
|
|
783
801
|
const hydration = this.getHydrationState();
|
|
784
802
|
const previous = hydration?.layers ?? [];
|
|
@@ -790,6 +808,7 @@ var ReactBrowserProvider = class {
|
|
|
790
808
|
hydration
|
|
791
809
|
});
|
|
792
810
|
window.addEventListener("popstate", () => {
|
|
811
|
+
if (this.state.pathname === location.pathname) return;
|
|
793
812
|
this.render();
|
|
794
813
|
});
|
|
795
814
|
}
|
|
@@ -805,6 +824,7 @@ var ReactBrowserRenderer = class {
|
|
|
805
824
|
env = $inject(envSchema);
|
|
806
825
|
log = $logger();
|
|
807
826
|
root;
|
|
827
|
+
options = { scrollRestoration: "top" };
|
|
808
828
|
getRootElement() {
|
|
809
829
|
const root = this.browserProvider.document.getElementById(this.env.REACT_ROOT_ID);
|
|
810
830
|
if (root) return root;
|
|
@@ -814,7 +834,7 @@ var ReactBrowserRenderer = class {
|
|
|
814
834
|
return div;
|
|
815
835
|
}
|
|
816
836
|
ready = $hook({
|
|
817
|
-
|
|
837
|
+
on: "react:browser:render",
|
|
818
838
|
handler: async ({ state, context, hydration }) => {
|
|
819
839
|
const element = this.browserRouterProvider.root(state, context);
|
|
820
840
|
if (hydration?.layers) {
|
|
@@ -827,17 +847,32 @@ var ReactBrowserRenderer = class {
|
|
|
827
847
|
}
|
|
828
848
|
}
|
|
829
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
|
+
});
|
|
830
856
|
};
|
|
831
857
|
|
|
832
858
|
//#endregion
|
|
833
859
|
//#region src/hooks/RouterHookApi.ts
|
|
834
860
|
var RouterHookApi = class {
|
|
835
|
-
constructor(pages, state, layer, browser) {
|
|
861
|
+
constructor(pages, context, state, layer, browser) {
|
|
836
862
|
this.pages = pages;
|
|
863
|
+
this.context = context;
|
|
837
864
|
this.state = state;
|
|
838
865
|
this.layer = layer;
|
|
839
866
|
this.browser = browser;
|
|
840
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
|
+
}
|
|
841
876
|
get current() {
|
|
842
877
|
return this.state;
|
|
843
878
|
}
|
|
@@ -915,7 +950,7 @@ const useRouter = () => {
|
|
|
915
950
|
const pages = useMemo(() => {
|
|
916
951
|
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
917
952
|
}, []);
|
|
918
|
-
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]);
|
|
919
954
|
};
|
|
920
955
|
|
|
921
956
|
//#endregion
|
|
@@ -1036,10 +1071,10 @@ const useRouterState = () => {
|
|
|
1036
1071
|
//#region src/index.browser.ts
|
|
1037
1072
|
var AlephaReact = class {
|
|
1038
1073
|
name = "alepha.react";
|
|
1039
|
-
$services = (alepha) => alepha.with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider).with(ReactBrowserRenderer);
|
|
1074
|
+
$services = (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider).with(ReactBrowserRenderer);
|
|
1040
1075
|
};
|
|
1041
1076
|
__bind($page, AlephaReact);
|
|
1042
1077
|
|
|
1043
1078
|
//#endregion
|
|
1044
|
-
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 };
|
|
1045
1080
|
//# sourceMappingURL=index.browser.js.map
|