@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 CHANGED
@@ -1 +1,32 @@
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
+ ## API Reference
27
+
28
+ ### Descriptors
29
+
30
+ #### $page()
31
+
32
+ Main descriptor for defining a React route in the application.
@@ -1,4 +1,4 @@
1
- import { $hook, $inject, $logger, Alepha, KIND, NotImplementedError, OPTIONS, __bind, __descriptor, t } from "@alepha/core";
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
- __descriptor(KEY);
16
- return {
17
- [KIND]: KEY,
18
- [OPTIONS]: options,
19
- render: () => {
20
- throw new NotImplementedError(KEY);
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] = KEY;
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 = $inject(envSchema$1);
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) : 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) : 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.getDescriptorValues($page);
550
+ const pages = this.alepha.descriptors($page);
545
551
  const hasParent = (it) => {
546
552
  for (const page of pages) {
547
- const children = page.value[OPTIONS].children ? Array.isArray(page.value[OPTIONS].children) ? page.value[OPTIONS].children : page.value[OPTIONS].children() : [];
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 { value, key } of pages) value[OPTIONS].name ??= key;
552
- for (const { value } of pages) {
553
- if (hasParent(value)) continue;
554
- if (value[OPTIONS].path === "/*") hasNotFoundHandler = true;
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[OPTIONS].children ? Array.isArray(target[OPTIONS].children) ? target[OPTIONS].children : target[OPTIONS].children() : [];
574
+ const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
570
575
  return {
571
- ...target[OPTIONS],
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
- return window.location.pathname + window.location.search;
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.history.replaceState({}, "", result.context.url.pathname);
771
+ this.pushState(result.context.url.pathname);
752
772
  return;
753
773
  }
754
774
  if (options.replace) {
755
- this.history.replaceState({}, "", url);
775
+ this.pushState(url);
756
776
  return;
757
777
  }
758
- this.history.pushState({}, "", url);
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 = $inject(envSchema);
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.get(PageDescriptorProvider).getPages();
955
+ return ctx.alepha.inject(PageDescriptorProvider).getPages();
919
956
  }, []);
920
- return useMemo(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
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[OPTIONS].path;
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[OPTIONS].can;
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[OPTIONS].name;
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.get(clazz), []);
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
- var AlephaReact = class {
1040
- name = "alepha.react";
1041
- $services = (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider).with(ReactBrowserRenderer);
1042
- };
1043
- __bind($page, AlephaReact);
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