@alepha/react 0.9.2 → 0.9.3

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.
@@ -1,4 +1,4 @@
1
- import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, NotImplementedError, createDescriptor, t } from "@alepha/core";
1
+ import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
2
2
  import { AlephaServer, HttpClient } from "@alepha/server";
3
3
  import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
4
4
  import { RouterProvider } from "@alepha/router";
@@ -14,6 +14,12 @@ const $page = (options) => {
14
14
  return createDescriptor(PageDescriptor, options);
15
15
  };
16
16
  var PageDescriptor = class extends Descriptor {
17
+ onInit() {
18
+ if (this.options.static) this.options.cache ??= {
19
+ provider: "memory",
20
+ ttl: [1, "week"]
21
+ };
22
+ }
17
23
  get name() {
18
24
  return this.options.name ?? this.config.propertyKey;
19
25
  }
@@ -22,14 +28,14 @@ var PageDescriptor = class extends Descriptor {
22
28
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
23
29
  */
24
30
  async render(options) {
25
- throw new NotImplementedError("");
31
+ throw new Error("render method is not implemented in this environment");
26
32
  }
27
33
  };
28
34
  $page[KIND] = PageDescriptor;
29
35
 
30
36
  //#endregion
31
37
  //#region src/components/NotFound.tsx
32
- function NotFoundPage() {
38
+ function NotFoundPage(props) {
33
39
  return /* @__PURE__ */ jsx("div", {
34
40
  style: {
35
41
  height: "100vh",
@@ -39,7 +45,8 @@ function NotFoundPage() {
39
45
  alignItems: "center",
40
46
  textAlign: "center",
41
47
  fontFamily: "sans-serif",
42
- padding: "1rem"
48
+ padding: "1rem",
49
+ ...props.style
43
50
  },
44
51
  children: /* @__PURE__ */ jsx("h1", {
45
52
  style: {
@@ -100,7 +107,7 @@ const ErrorViewer = ({ error, alepha }) => {
100
107
  heading: {
101
108
  fontSize: "20px",
102
109
  fontWeight: "bold",
103
- marginBottom: "4px"
110
+ marginBottom: "10px"
104
111
  },
105
112
  name: {
106
113
  fontSize: "16px",
@@ -317,21 +324,24 @@ const NestedView = (props) => {
317
324
  const layer = useContext(RouterLayerContext);
318
325
  const index = layer?.index ?? 0;
319
326
  const [view, setView] = useState(app?.state.layers[index]?.element);
320
- useRouterEvents({ onEnd: ({ state }) => {
327
+ useRouterEvents({ onEnd: ({ state, context }) => {
328
+ if (app) app.context = context;
321
329
  if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
322
330
  } }, [app]);
323
331
  if (!app) throw new Error("NestedView must be used within a RouterContext.");
324
332
  const element = view ?? props.children ?? null;
325
333
  return /* @__PURE__ */ jsx(ErrorBoundary_default, {
326
- fallback: app.context.onError,
334
+ fallback: (error) => {
335
+ return app.context.onError?.(error, app.context);
336
+ },
327
337
  children: element
328
338
  });
329
339
  };
330
340
  var NestedView_default = NestedView;
331
341
 
332
342
  //#endregion
333
- //#region src/errors/RedirectionError.ts
334
- var RedirectionError = class extends Error {
343
+ //#region src/errors/Redirection.ts
344
+ var Redirection = class extends Error {
335
345
  page;
336
346
  constructor(page) {
337
347
  super("Redirection");
@@ -354,7 +364,7 @@ var PageDescriptorProvider = class {
354
364
  for (const page of this.pages) if (page.name === name) return page;
355
365
  throw new Error(`Page ${name} not found`);
356
366
  }
357
- url(name, options = {}) {
367
+ pathname(name, options = {}) {
358
368
  const page = this.page(name);
359
369
  if (!page) throw new Error(`Page ${name} not found`);
360
370
  let url = page.path ?? "";
@@ -364,7 +374,14 @@ var PageDescriptorProvider = class {
364
374
  parent = parent.parent;
365
375
  }
366
376
  url = this.compile(url, options.params ?? {});
367
- return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
377
+ if (options.query) {
378
+ const query = new URLSearchParams(options.query);
379
+ if (query.toString()) url += `?${query.toString()}`;
380
+ }
381
+ return url.replace(/\/\/+/g, "/") || "/";
382
+ }
383
+ url(name, options = {}) {
384
+ return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
368
385
  }
369
386
  root(state, context) {
370
387
  const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(RouterContext.Provider, { value: {
@@ -439,12 +456,10 @@ var PageDescriptorProvider = class {
439
456
  ...props
440
457
  };
441
458
  } catch (e) {
442
- if (e instanceof RedirectionError) return {
443
- layers: [],
444
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
459
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
445
460
  pathname,
446
461
  search
447
- };
462
+ });
448
463
  this.log.error(e);
449
464
  it.error = e;
450
465
  break;
@@ -460,9 +475,21 @@ var PageDescriptorProvider = class {
460
475
  acc += it.route.path ? this.compile(it.route.path, params) : "";
461
476
  const path = acc.replace(/\/+/, "/");
462
477
  const localErrorHandler = this.getErrorHandler(it.route);
463
- if (localErrorHandler) request.onError = localErrorHandler;
464
- if (it.error) {
465
- let element$1 = await request.onError(it.error);
478
+ if (localErrorHandler) {
479
+ const onErrorParent = request.onError;
480
+ request.onError = (error, context$1) => {
481
+ const result = localErrorHandler(error, context$1);
482
+ if (result === void 0) return onErrorParent(error, context$1);
483
+ return result;
484
+ };
485
+ }
486
+ if (it.error) try {
487
+ let element$1 = await request.onError(it.error, request);
488
+ if (element$1 === void 0) throw it.error;
489
+ if (element$1 instanceof Redirection) return this.createRedirectionLayer(element$1.page, {
490
+ pathname,
491
+ search
492
+ });
466
493
  if (element$1 === null) element$1 = this.renderError(it.error);
467
494
  layers.push({
468
495
  props,
@@ -476,6 +503,12 @@ var PageDescriptorProvider = class {
476
503
  route: it.route
477
504
  });
478
505
  break;
506
+ } catch (e) {
507
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
508
+ pathname,
509
+ search
510
+ });
511
+ throw e;
479
512
  }
480
513
  const element = await this.createElement(it.route, {
481
514
  ...props,
@@ -499,6 +532,14 @@ var PageDescriptorProvider = class {
499
532
  search
500
533
  };
501
534
  }
535
+ createRedirectionLayer(href, context) {
536
+ return {
537
+ layers: [],
538
+ redirect: typeof href === "string" ? href : this.href(href),
539
+ pathname: context.pathname,
540
+ search: context.search
541
+ };
542
+ }
502
543
  getErrorHandler(route) {
503
544
  if (route.errorHandler) return route.errorHandler;
504
545
  let parent = route.parent;
@@ -554,6 +595,7 @@ var PageDescriptorProvider = class {
554
595
  let hasNotFoundHandler = false;
555
596
  const pages = this.alepha.descriptors($page);
556
597
  const hasParent = (it) => {
598
+ if (it.options.parent) return true;
557
599
  for (const page of pages) {
558
600
  const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
559
601
  if (children.includes(it)) return true;
@@ -569,7 +611,7 @@ var PageDescriptorProvider = class {
569
611
  name: "notFound",
570
612
  cache: true,
571
613
  component: NotFoundPage,
572
- afterHandler: ({ reply }) => {
614
+ onServerResponse: ({ reply }) => {
573
615
  reply.status = 404;
574
616
  }
575
617
  });
@@ -577,6 +619,12 @@ var PageDescriptorProvider = class {
577
619
  });
578
620
  map(pages, target) {
579
621
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
622
+ const getChildrenFromParent = (it) => {
623
+ const children$1 = [];
624
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
625
+ return children$1;
626
+ };
627
+ children.push(...getChildrenFromParent(target));
580
628
  return {
581
629
  ...target.options,
582
630
  name: target.name,
@@ -698,6 +746,10 @@ var BrowserRouterProvider = class extends RouterProvider {
698
746
  options.state.pathname = state.pathname;
699
747
  options.state.search = state.search;
700
748
  }
749
+ if (options.previous) for (let i = 0; i < options.previous.length; i++) {
750
+ const layer = options.previous[i];
751
+ if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
752
+ }
701
753
  await this.alepha.emit("react:transition:end", {
702
754
  state: options.state,
703
755
  context
@@ -772,15 +824,11 @@ var ReactBrowserProvider = class {
772
824
  }
773
825
  async go(url, options = {}) {
774
826
  const result = await this.render({ url });
775
- if (result.context.url.pathname !== url) {
776
- this.pushState(result.context.url.pathname);
827
+ if (result.context.url.pathname + result.context.url.search !== url) {
828
+ this.pushState(result.context.url.pathname + result.context.url.search);
777
829
  return;
778
830
  }
779
- if (options.replace) {
780
- this.pushState(url);
781
- return;
782
- }
783
- this.pushState(url);
831
+ this.pushState(url, options.replace);
784
832
  }
785
833
  async render(options = {}) {
786
834
  const previous = options.previous ?? this.state.layers;
@@ -809,7 +857,13 @@ var ReactBrowserProvider = class {
809
857
  handler: async () => {
810
858
  const hydration = this.getHydrationState();
811
859
  const previous = hydration?.layers ?? [];
812
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
860
+ if (hydration) {
861
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
862
+ }
863
+ if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
864
+ ...link,
865
+ prefix: hydration.links.prefix
866
+ });
813
867
  const { context } = await this.render({ previous });
814
868
  await this.alepha.emit("react:browser:render", {
815
869
  state: this.state,
@@ -867,13 +921,23 @@ var ReactBrowserRenderer = class {
867
921
  //#endregion
868
922
  //#region src/hooks/RouterHookApi.ts
869
923
  var RouterHookApi = class {
870
- constructor(pages, context, state, layer, browser) {
924
+ constructor(pages, context, state, layer, pageApi, browser) {
871
925
  this.pages = pages;
872
926
  this.context = context;
873
927
  this.state = state;
874
928
  this.layer = layer;
929
+ this.pageApi = pageApi;
875
930
  this.browser = browser;
876
931
  }
932
+ path(name, config = {}) {
933
+ return this.pageApi.pathname(name, {
934
+ params: {
935
+ ...this.context.params,
936
+ ...config.params
937
+ },
938
+ query: config.query
939
+ });
940
+ }
877
941
  getURL() {
878
942
  if (!this.browser) return this.context.url;
879
943
  return new URL(this.location.href);
@@ -915,23 +979,23 @@ var RouterHookApi = class {
915
979
  }
916
980
  async go(path, options) {
917
981
  for (const page of this.pages) if (page.name === path) {
918
- path = page.path ?? "";
919
- break;
982
+ await this.browser?.go(this.path(path, options), options);
983
+ return;
920
984
  }
921
- await this.browser?.go(this.createHref(path, this.layer, options), options);
985
+ await this.browser?.go(path, options);
922
986
  }
923
987
  anchor(path, options = {}) {
988
+ let href = path;
924
989
  for (const page of this.pages) if (page.name === path) {
925
- path = page.path ?? "";
990
+ href = this.path(path, options);
926
991
  break;
927
992
  }
928
- const href = this.createHref(path, this.layer, options);
929
993
  return {
930
994
  href,
931
995
  onClick: (ev) => {
932
996
  ev.stopPropagation();
933
997
  ev.preventDefault();
934
- this.go(path, options).catch(console.error);
998
+ this.go(href, options).catch(console.error);
935
999
  }
936
1000
  };
937
1001
  }
@@ -960,27 +1024,18 @@ const useRouter = () => {
960
1024
  const pages = useMemo(() => {
961
1025
  return alepha.inject(PageDescriptorProvider).getPages();
962
1026
  }, []);
963
- return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1027
+ return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
964
1028
  };
965
1029
 
966
1030
  //#endregion
967
1031
  //#region src/components/Link.tsx
968
1032
  const Link = (props) => {
969
- React.useContext(RouterContext);
970
1033
  const router = useRouter();
971
- const to = typeof props.to === "string" ? props.to : props.to.options.path;
972
- if (!to) return null;
973
- const can = typeof props.to === "string" ? void 0 : props.to.options.can;
974
- if (can && !can()) return null;
975
- const name = typeof props.to === "string" ? void 0 : props.to.options.name;
976
- const anchorProps = {
977
- ...props,
978
- to: void 0
979
- };
1034
+ const { to,...anchorProps } = props;
980
1035
  return /* @__PURE__ */ jsx("a", {
981
1036
  ...router.anchor(to),
982
1037
  ...anchorProps,
983
- children: props.children ?? name
1038
+ children: props.children
984
1039
  });
985
1040
  };
986
1041
  var Link_default = Link;
@@ -992,22 +1047,21 @@ const useActive = (path) => {
992
1047
  const ctx = useContext(RouterContext);
993
1048
  const layer = useContext(RouterLayerContext);
994
1049
  if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
995
- let name;
996
- if (typeof path === "object" && path.options.name) name = path.options.name;
997
1050
  const [current, setCurrent] = useState(ctx.state.pathname);
998
- const href = useMemo(() => router.createHref(path, layer), [path, layer]);
1051
+ const href = useMemo(() => router.createHref(path ?? "", layer), [path, layer]);
999
1052
  const [isPending, setPending] = useState(false);
1000
- const isActive = current === href;
1001
- useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
1053
+ const isActive = current === href || current === `${href}/` || `${current}/` === href;
1054
+ useRouterEvents({ onEnd: ({ state }) => {
1055
+ path && setCurrent(state.pathname);
1056
+ } }, [path]);
1002
1057
  return {
1003
- name,
1004
1058
  isPending,
1005
1059
  isActive,
1006
1060
  anchorProps: {
1007
1061
  href,
1008
1062
  onClick: (ev) => {
1009
- ev.stopPropagation();
1010
- ev.preventDefault();
1063
+ ev?.stopPropagation();
1064
+ ev?.preventDefault();
1011
1065
  if (isActive) return;
1012
1066
  if (isPending) return;
1013
1067
  setPending(true);
@@ -1026,9 +1080,36 @@ const useInject = (service) => {
1026
1080
  return useMemo(() => alepha.inject(service), []);
1027
1081
  };
1028
1082
 
1083
+ //#endregion
1084
+ //#region src/hooks/useStore.ts
1085
+ /**
1086
+ * Hook to access and mutate the Alepha state.
1087
+ */
1088
+ const useStore = (key, defaultValue) => {
1089
+ const alepha = useAlepha();
1090
+ useMemo(() => {
1091
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1092
+ }, [defaultValue]);
1093
+ const [state, setState] = useState(alepha.state(key));
1094
+ useEffect(() => {
1095
+ if (!alepha.isBrowser()) return;
1096
+ return alepha.on("state:mutate", (ev) => {
1097
+ if (ev.key === key) setState(ev.value);
1098
+ });
1099
+ }, []);
1100
+ if (!alepha.isBrowser()) {
1101
+ const value = alepha.context.get(key);
1102
+ if (value !== null) return [value, (_) => {}];
1103
+ }
1104
+ return [state, (value) => {
1105
+ alepha.state(key, value);
1106
+ }];
1107
+ };
1108
+
1029
1109
  //#endregion
1030
1110
  //#region src/hooks/useClient.ts
1031
1111
  const useClient = (_scope) => {
1112
+ useStore("user");
1032
1113
  return useInject(LinkProvider).client();
1033
1114
  };
1034
1115
 
@@ -1111,29 +1192,6 @@ const ssrSchemaLoading = (alepha, name) => {
1111
1192
  return { loading: true };
1112
1193
  };
1113
1194
 
1114
- //#endregion
1115
- //#region src/hooks/useStore.ts
1116
- /**
1117
- * Hook to access and mutate the Alepha state.
1118
- */
1119
- const useStore = (key) => {
1120
- const alepha = useAlepha();
1121
- const [state, setState] = useState(alepha.state(key));
1122
- useEffect(() => {
1123
- if (!alepha.isBrowser()) return;
1124
- return alepha.on("state:mutate", (ev) => {
1125
- if (ev.key === key) setState(ev.value);
1126
- });
1127
- }, []);
1128
- if (!alepha.isBrowser()) {
1129
- const value = alepha.context.get(key);
1130
- if (value !== null) return [value, (_) => {}];
1131
- }
1132
- return [state, (value) => {
1133
- alepha.state(key, value);
1134
- }];
1135
- };
1136
-
1137
1195
  //#endregion
1138
1196
  //#region src/index.browser.ts
1139
1197
  const AlephaReact = $module({
@@ -1149,5 +1207,5 @@ const AlephaReact = $module({
1149
1207
  });
1150
1208
 
1151
1209
  //#endregion
1152
- export { $page, AlephaContext, 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, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1210
+ export { $page, AlephaContext, AlephaReact, BrowserRouterProvider, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, Redirection, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1153
1211
  //# sourceMappingURL=index.browser.js.map