@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 CHANGED
@@ -1 +1,153 @@
1
- # alepha/react
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.
@@ -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) : 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) : 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
- return window.location.pathname + window.location.search;
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.history.replaceState({}, "", result.context.url.pathname);
767
+ this.pushState(result.context.url.pathname);
752
768
  return;
753
769
  }
754
770
  if (options.replace) {
755
- this.history.replaceState({}, "", url);
771
+ this.pushState(url);
756
772
  return;
757
773
  }
758
- this.history.pushState({}, "", url);
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