@alepha/react 0.9.1 → 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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, NotImplementedError, createDescriptor, t } from "@alepha/core";
2
- import { AlephaServer, ServerRouterProvider, ServerTimingProvider, apiLinksResponseSchema } from "@alepha/server";
1
+ import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
2
+ import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider, apiLinksResponseSchema } from "@alepha/server";
3
3
  import { AlephaServerCache } from "@alepha/server-cache";
4
4
  import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
5
5
  import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
@@ -18,6 +18,12 @@ const $page = (options) => {
18
18
  return createDescriptor(PageDescriptor, options);
19
19
  };
20
20
  var PageDescriptor = class extends Descriptor {
21
+ onInit() {
22
+ if (this.options.static) this.options.cache ??= {
23
+ provider: "memory",
24
+ ttl: [1, "week"]
25
+ };
26
+ }
21
27
  get name() {
22
28
  return this.options.name ?? this.config.propertyKey;
23
29
  }
@@ -26,7 +32,7 @@ var PageDescriptor = class extends Descriptor {
26
32
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
27
33
  */
28
34
  async render(options) {
29
- throw new NotImplementedError("");
35
+ throw new Error("render method is not implemented in this environment");
30
36
  }
31
37
  };
32
38
  $page[KIND] = PageDescriptor;
@@ -80,7 +86,7 @@ const ErrorViewer = ({ error, alepha }) => {
80
86
  heading: {
81
87
  fontSize: "20px",
82
88
  fontWeight: "bold",
83
- marginBottom: "4px"
89
+ marginBottom: "10px"
84
90
  },
85
91
  name: {
86
92
  fontSize: "16px",
@@ -207,20 +213,31 @@ const RouterContext = createContext(void 0);
207
213
  //#region src/contexts/RouterLayerContext.ts
208
214
  const RouterLayerContext = createContext(void 0);
209
215
 
216
+ //#endregion
217
+ //#region src/contexts/AlephaContext.ts
218
+ const AlephaContext = createContext(void 0);
219
+
220
+ //#endregion
221
+ //#region src/hooks/useAlepha.ts
222
+ const useAlepha = () => {
223
+ const alepha = useContext(AlephaContext);
224
+ if (!alepha) throw new Error("useAlepha must be used within an AlephaContext.Provider");
225
+ return alepha;
226
+ };
227
+
210
228
  //#endregion
211
229
  //#region src/hooks/useRouterEvents.ts
212
230
  const useRouterEvents = (opts = {}, deps = []) => {
213
- const ctx = useContext(RouterContext);
214
- if (!ctx) throw new Error("useRouter must be used within a RouterProvider");
231
+ const alepha = useAlepha();
215
232
  useEffect(() => {
216
- if (!ctx.alepha.isBrowser()) return;
233
+ if (!alepha.isBrowser()) return;
217
234
  const subs = [];
218
235
  const onBegin = opts.onBegin;
219
236
  const onEnd = opts.onEnd;
220
237
  const onError = opts.onError;
221
- if (onBegin) subs.push(ctx.alepha.on("react:transition:begin", { callback: onBegin }));
222
- if (onEnd) subs.push(ctx.alepha.on("react:transition:end", { callback: onEnd }));
223
- if (onError) subs.push(ctx.alepha.on("react:transition:error", { callback: onError }));
238
+ if (onBegin) subs.push(alepha.on("react:transition:begin", { callback: onBegin }));
239
+ if (onEnd) subs.push(alepha.on("react:transition:end", { callback: onEnd }));
240
+ if (onError) subs.push(alepha.on("react:transition:error", { callback: onError }));
224
241
  return () => {
225
242
  for (const sub of subs) sub();
226
243
  };
@@ -286,13 +303,16 @@ const NestedView = (props) => {
286
303
  const layer = useContext(RouterLayerContext);
287
304
  const index = layer?.index ?? 0;
288
305
  const [view, setView] = useState(app?.state.layers[index]?.element);
289
- useRouterEvents({ onEnd: ({ state }) => {
306
+ useRouterEvents({ onEnd: ({ state, context }) => {
307
+ if (app) app.context = context;
290
308
  if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
291
309
  } }, [app]);
292
310
  if (!app) throw new Error("NestedView must be used within a RouterContext.");
293
311
  const element = view ?? props.children ?? null;
294
312
  return /* @__PURE__ */ jsx(ErrorBoundary_default, {
295
- fallback: app.context.onError,
313
+ fallback: (error) => {
314
+ return app.context.onError?.(error, app.context);
315
+ },
296
316
  children: element
297
317
  });
298
318
  };
@@ -300,7 +320,7 @@ var NestedView_default = NestedView;
300
320
 
301
321
  //#endregion
302
322
  //#region src/components/NotFound.tsx
303
- function NotFoundPage() {
323
+ function NotFoundPage(props) {
304
324
  return /* @__PURE__ */ jsx("div", {
305
325
  style: {
306
326
  height: "100vh",
@@ -310,7 +330,8 @@ function NotFoundPage() {
310
330
  alignItems: "center",
311
331
  textAlign: "center",
312
332
  fontFamily: "sans-serif",
313
- padding: "1rem"
333
+ padding: "1rem",
334
+ ...props.style
314
335
  },
315
336
  children: /* @__PURE__ */ jsx("h1", {
316
337
  style: {
@@ -323,8 +344,8 @@ function NotFoundPage() {
323
344
  }
324
345
 
325
346
  //#endregion
326
- //#region src/errors/RedirectionError.ts
327
- var RedirectionError = class extends Error {
347
+ //#region src/errors/Redirection.ts
348
+ var Redirection = class extends Error {
328
349
  page;
329
350
  constructor(page) {
330
351
  super("Redirection");
@@ -347,7 +368,7 @@ var PageDescriptorProvider = class {
347
368
  for (const page of this.pages) if (page.name === name) return page;
348
369
  throw new Error(`Page ${name} not found`);
349
370
  }
350
- url(name, options = {}) {
371
+ pathname(name, options = {}) {
351
372
  const page = this.page(name);
352
373
  if (!page) throw new Error(`Page ${name} not found`);
353
374
  let url = page.path ?? "";
@@ -357,14 +378,20 @@ var PageDescriptorProvider = class {
357
378
  parent = parent.parent;
358
379
  }
359
380
  url = this.compile(url, options.params ?? {});
360
- return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
381
+ if (options.query) {
382
+ const query = new URLSearchParams(options.query);
383
+ if (query.toString()) url += `?${query.toString()}`;
384
+ }
385
+ return url.replace(/\/\/+/g, "/") || "/";
386
+ }
387
+ url(name, options = {}) {
388
+ return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
361
389
  }
362
390
  root(state, context) {
363
- const root = createElement(RouterContext.Provider, { value: {
364
- alepha: this.alepha,
391
+ const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(RouterContext.Provider, { value: {
365
392
  state,
366
393
  context
367
- } }, createElement(NestedView_default, {}, state.layers[0]?.element));
394
+ } }, createElement(NestedView_default, {}, state.layers[0]?.element)));
368
395
  if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
369
396
  return root;
370
397
  }
@@ -433,12 +460,10 @@ var PageDescriptorProvider = class {
433
460
  ...props
434
461
  };
435
462
  } catch (e) {
436
- if (e instanceof RedirectionError) return {
437
- layers: [],
438
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
463
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
439
464
  pathname,
440
465
  search
441
- };
466
+ });
442
467
  this.log.error(e);
443
468
  it.error = e;
444
469
  break;
@@ -454,9 +479,21 @@ var PageDescriptorProvider = class {
454
479
  acc += it.route.path ? this.compile(it.route.path, params) : "";
455
480
  const path = acc.replace(/\/+/, "/");
456
481
  const localErrorHandler = this.getErrorHandler(it.route);
457
- if (localErrorHandler) request.onError = localErrorHandler;
458
- if (it.error) {
459
- let element$1 = await request.onError(it.error);
482
+ if (localErrorHandler) {
483
+ const onErrorParent = request.onError;
484
+ request.onError = (error, context$1) => {
485
+ const result = localErrorHandler(error, context$1);
486
+ if (result === void 0) return onErrorParent(error, context$1);
487
+ return result;
488
+ };
489
+ }
490
+ if (it.error) try {
491
+ let element$1 = await request.onError(it.error, request);
492
+ if (element$1 === void 0) throw it.error;
493
+ if (element$1 instanceof Redirection) return this.createRedirectionLayer(element$1.page, {
494
+ pathname,
495
+ search
496
+ });
460
497
  if (element$1 === null) element$1 = this.renderError(it.error);
461
498
  layers.push({
462
499
  props,
@@ -470,6 +507,12 @@ var PageDescriptorProvider = class {
470
507
  route: it.route
471
508
  });
472
509
  break;
510
+ } catch (e) {
511
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
512
+ pathname,
513
+ search
514
+ });
515
+ throw e;
473
516
  }
474
517
  const element = await this.createElement(it.route, {
475
518
  ...props,
@@ -493,6 +536,14 @@ var PageDescriptorProvider = class {
493
536
  search
494
537
  };
495
538
  }
539
+ createRedirectionLayer(href, context) {
540
+ return {
541
+ layers: [],
542
+ redirect: typeof href === "string" ? href : this.href(href),
543
+ pathname: context.pathname,
544
+ search: context.search
545
+ };
546
+ }
496
547
  getErrorHandler(route) {
497
548
  if (route.errorHandler) return route.errorHandler;
498
549
  let parent = route.parent;
@@ -548,6 +599,7 @@ var PageDescriptorProvider = class {
548
599
  let hasNotFoundHandler = false;
549
600
  const pages = this.alepha.descriptors($page);
550
601
  const hasParent = (it) => {
602
+ if (it.options.parent) return true;
551
603
  for (const page of pages) {
552
604
  const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
553
605
  if (children.includes(it)) return true;
@@ -563,7 +615,7 @@ var PageDescriptorProvider = class {
563
615
  name: "notFound",
564
616
  cache: true,
565
617
  component: NotFoundPage,
566
- afterHandler: ({ reply }) => {
618
+ onServerResponse: ({ reply }) => {
567
619
  reply.status = 404;
568
620
  }
569
621
  });
@@ -571,6 +623,12 @@ var PageDescriptorProvider = class {
571
623
  });
572
624
  map(pages, target) {
573
625
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
626
+ const getChildrenFromParent = (it) => {
627
+ const children$1 = [];
628
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
629
+ return children$1;
630
+ };
631
+ children.push(...getChildrenFromParent(target));
574
632
  return {
575
633
  ...target.options,
576
634
  name: target.name,
@@ -692,6 +750,10 @@ var BrowserRouterProvider = class extends RouterProvider {
692
750
  options.state.pathname = state.pathname;
693
751
  options.state.search = state.search;
694
752
  }
753
+ if (options.previous) for (let i = 0; i < options.previous.length; i++) {
754
+ const layer = options.previous[i];
755
+ if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
756
+ }
695
757
  await this.alepha.emit("react:transition:end", {
696
758
  state: options.state,
697
759
  context
@@ -766,15 +828,11 @@ var ReactBrowserProvider = class {
766
828
  }
767
829
  async go(url, options = {}) {
768
830
  const result = await this.render({ url });
769
- if (result.context.url.pathname !== url) {
770
- this.pushState(result.context.url.pathname);
771
- return;
772
- }
773
- if (options.replace) {
774
- this.pushState(url);
831
+ if (result.context.url.pathname + result.context.url.search !== url) {
832
+ this.pushState(result.context.url.pathname + result.context.url.search);
775
833
  return;
776
834
  }
777
- this.pushState(url);
835
+ this.pushState(url, options.replace);
778
836
  }
779
837
  async render(options = {}) {
780
838
  const previous = options.previous ?? this.state.layers;
@@ -803,7 +861,13 @@ var ReactBrowserProvider = class {
803
861
  handler: async () => {
804
862
  const hydration = this.getHydrationState();
805
863
  const previous = hydration?.layers ?? [];
806
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
864
+ if (hydration) {
865
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
866
+ }
867
+ if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
868
+ ...link,
869
+ prefix: hydration.links.prefix
870
+ });
807
871
  const { context } = await this.render({ previous });
808
872
  await this.alepha.emit("react:browser:render", {
809
873
  state: this.state,
@@ -931,6 +995,7 @@ var ReactServerProvider = class {
931
995
  html: renderToString(this.pageDescriptorProvider.root(state, context))
932
996
  };
933
997
  const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
998
+ if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
934
999
  const result = {
935
1000
  context,
936
1001
  state,
@@ -945,6 +1010,7 @@ var ReactServerProvider = class {
945
1010
  const { url, reply, query, params } = serverRequest;
946
1011
  const template = await templateLoader();
947
1012
  if (!template) throw new Error("Template not found");
1013
+ this.log.trace("Rendering page", { name: page.name });
948
1014
  const context = {
949
1015
  url,
950
1016
  params,
@@ -970,6 +1036,10 @@ var ReactServerProvider = class {
970
1036
  }
971
1037
  target = target.parent;
972
1038
  }
1039
+ await this.alepha.emit("react:transition:begin", {
1040
+ request: serverRequest,
1041
+ context
1042
+ });
973
1043
  await this.alepha.emit("react:server:render:begin", {
974
1044
  request: serverRequest,
975
1045
  context
@@ -984,14 +1054,20 @@ var ReactServerProvider = class {
984
1054
  reply.headers.expires = "0";
985
1055
  if (page.cache && serverRequest.user) delete context.links;
986
1056
  const html = this.renderToHtml(template, state, context);
987
- await this.alepha.emit("react:server:render:end", {
1057
+ if (html instanceof Redirection) {
1058
+ reply.redirect(typeof html.page === "string" ? html.page : this.pageDescriptorProvider.href(html.page));
1059
+ return;
1060
+ }
1061
+ const event = {
988
1062
  request: serverRequest,
989
1063
  context,
990
1064
  state,
991
1065
  html
992
- });
993
- page.afterHandler?.(serverRequest);
994
- return html;
1066
+ };
1067
+ await this.alepha.emit("react:server:render:end", event);
1068
+ page.onServerResponse?.(serverRequest);
1069
+ this.log.trace("Page rendered", { name: page.name });
1070
+ return event.html;
995
1071
  };
996
1072
  }
997
1073
  renderToHtml(template, state, context, hydration = true) {
@@ -1002,20 +1078,23 @@ var ReactServerProvider = class {
1002
1078
  app = renderToString(element);
1003
1079
  } catch (error) {
1004
1080
  this.log.error("Error during SSR", error);
1005
- app = renderToString(context.onError(error));
1081
+ const element$1 = context.onError(error, context);
1082
+ if (element$1 instanceof Redirection) return element$1;
1083
+ app = renderToString(element$1);
1006
1084
  }
1007
1085
  this.serverTimingProvider.endTiming("renderToString");
1008
1086
  const response = { html: template };
1009
1087
  if (hydration) {
1088
+ const { request, context: context$1,...rest } = this.alepha.context.als?.getStore() ?? {};
1010
1089
  const hydrationData = {
1011
- links: context.links,
1090
+ ...rest,
1012
1091
  layers: state.layers.map((it) => ({
1013
1092
  ...it,
1014
1093
  error: it.error ? {
1015
1094
  ...it.error,
1016
1095
  name: it.error.name,
1017
1096
  message: it.error.message,
1018
- stack: it.error.stack
1097
+ stack: !this.alepha.isProduction() ? it.error.stack : void 0
1019
1098
  } : void 0,
1020
1099
  index: void 0,
1021
1100
  path: void 0,
@@ -1035,24 +1114,34 @@ var ReactServerProvider = class {
1035
1114
  else {
1036
1115
  const bodyOpenTag = /<body([^>]*)>/i;
1037
1116
  if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1038
- return `${match}\n<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1117
+ return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1039
1118
  });
1040
1119
  }
1041
1120
  const bodyCloseTagRegex = /<\/body>/i;
1042
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}\n</body>`);
1121
+ if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
1043
1122
  }
1044
1123
  };
1045
1124
 
1046
1125
  //#endregion
1047
1126
  //#region src/hooks/RouterHookApi.ts
1048
1127
  var RouterHookApi = class {
1049
- constructor(pages, context, state, layer, browser) {
1128
+ constructor(pages, context, state, layer, pageApi, browser) {
1050
1129
  this.pages = pages;
1051
1130
  this.context = context;
1052
1131
  this.state = state;
1053
1132
  this.layer = layer;
1133
+ this.pageApi = pageApi;
1054
1134
  this.browser = browser;
1055
1135
  }
1136
+ path(name, config = {}) {
1137
+ return this.pageApi.pathname(name, {
1138
+ params: {
1139
+ ...this.context.params,
1140
+ ...config.params
1141
+ },
1142
+ query: config.query
1143
+ });
1144
+ }
1056
1145
  getURL() {
1057
1146
  if (!this.browser) return this.context.url;
1058
1147
  return new URL(this.location.href);
@@ -1094,23 +1183,23 @@ var RouterHookApi = class {
1094
1183
  }
1095
1184
  async go(path, options) {
1096
1185
  for (const page of this.pages) if (page.name === path) {
1097
- path = page.path ?? "";
1098
- break;
1186
+ await this.browser?.go(this.path(path, options), options);
1187
+ return;
1099
1188
  }
1100
- await this.browser?.go(this.createHref(path, this.layer, options), options);
1189
+ await this.browser?.go(path, options);
1101
1190
  }
1102
1191
  anchor(path, options = {}) {
1192
+ let href = path;
1103
1193
  for (const page of this.pages) if (page.name === path) {
1104
- path = page.path ?? "";
1194
+ href = this.path(path, options);
1105
1195
  break;
1106
1196
  }
1107
- const href = this.createHref(path, this.layer, options);
1108
1197
  return {
1109
1198
  href,
1110
1199
  onClick: (ev) => {
1111
1200
  ev.stopPropagation();
1112
1201
  ev.preventDefault();
1113
- this.go(path, options).catch(console.error);
1202
+ this.go(href, options).catch(console.error);
1114
1203
  }
1115
1204
  };
1116
1205
  }
@@ -1132,33 +1221,25 @@ var RouterHookApi = class {
1132
1221
  //#endregion
1133
1222
  //#region src/hooks/useRouter.ts
1134
1223
  const useRouter = () => {
1224
+ const alepha = useAlepha();
1135
1225
  const ctx = useContext(RouterContext);
1136
1226
  const layer = useContext(RouterLayerContext);
1137
1227
  if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1138
1228
  const pages = useMemo(() => {
1139
- return ctx.alepha.inject(PageDescriptorProvider).getPages();
1229
+ return alepha.inject(PageDescriptorProvider).getPages();
1140
1230
  }, []);
1141
- return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1231
+ return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1142
1232
  };
1143
1233
 
1144
1234
  //#endregion
1145
1235
  //#region src/components/Link.tsx
1146
1236
  const Link = (props) => {
1147
- React.useContext(RouterContext);
1148
1237
  const router = useRouter();
1149
- const to = typeof props.to === "string" ? props.to : props.to.options.path;
1150
- if (!to) return null;
1151
- const can = typeof props.to === "string" ? void 0 : props.to.options.can;
1152
- if (can && !can()) return null;
1153
- const name = typeof props.to === "string" ? void 0 : props.to.options.name;
1154
- const anchorProps = {
1155
- ...props,
1156
- to: void 0
1157
- };
1238
+ const { to,...anchorProps } = props;
1158
1239
  return /* @__PURE__ */ jsx("a", {
1159
1240
  ...router.anchor(to),
1160
1241
  ...anchorProps,
1161
- children: props.children ?? name
1242
+ children: props.children
1162
1243
  });
1163
1244
  };
1164
1245
  var Link_default = Link;
@@ -1170,22 +1251,21 @@ const useActive = (path) => {
1170
1251
  const ctx = useContext(RouterContext);
1171
1252
  const layer = useContext(RouterLayerContext);
1172
1253
  if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1173
- let name;
1174
- if (typeof path === "object" && path.options.name) name = path.options.name;
1175
1254
  const [current, setCurrent] = useState(ctx.state.pathname);
1176
- const href = useMemo(() => router.createHref(path, layer), [path, layer]);
1255
+ const href = useMemo(() => router.createHref(path ?? "", layer), [path, layer]);
1177
1256
  const [isPending, setPending] = useState(false);
1178
- const isActive = current === href;
1179
- useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
1257
+ const isActive = current === href || current === `${href}/` || `${current}/` === href;
1258
+ useRouterEvents({ onEnd: ({ state }) => {
1259
+ path && setCurrent(state.pathname);
1260
+ } }, [path]);
1180
1261
  return {
1181
- name,
1182
1262
  isPending,
1183
1263
  isActive,
1184
1264
  anchorProps: {
1185
1265
  href,
1186
1266
  onClick: (ev) => {
1187
- ev.stopPropagation();
1188
- ev.preventDefault();
1267
+ ev?.stopPropagation();
1268
+ ev?.preventDefault();
1189
1269
  if (isActive) return;
1190
1270
  if (isPending) return;
1191
1271
  setPending(true);
@@ -1198,45 +1278,62 @@ const useActive = (path) => {
1198
1278
  };
1199
1279
 
1200
1280
  //#endregion
1201
- //#region src/hooks/useAlepha.ts
1202
- const useAlepha = () => {
1203
- const routerContext = useContext(RouterContext);
1204
- if (!routerContext) throw new Error("useAlepha must be used within a RouterProvider");
1205
- return routerContext.alepha;
1281
+ //#region src/hooks/useInject.ts
1282
+ const useInject = (service) => {
1283
+ const alepha = useAlepha();
1284
+ return useMemo(() => alepha.inject(service), []);
1206
1285
  };
1207
1286
 
1208
1287
  //#endregion
1209
- //#region src/hooks/useInject.ts
1210
- const useInject = (clazz) => {
1211
- const ctx = useContext(RouterContext);
1212
- if (!ctx) throw new Error("useRouter must be used within a <RouterProvider>");
1213
- return useMemo(() => ctx.alepha.inject(clazz), []);
1288
+ //#region src/hooks/useStore.ts
1289
+ /**
1290
+ * Hook to access and mutate the Alepha state.
1291
+ */
1292
+ const useStore = (key, defaultValue) => {
1293
+ const alepha = useAlepha();
1294
+ useMemo(() => {
1295
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1296
+ }, [defaultValue]);
1297
+ const [state, setState] = useState(alepha.state(key));
1298
+ useEffect(() => {
1299
+ if (!alepha.isBrowser()) return;
1300
+ return alepha.on("state:mutate", (ev) => {
1301
+ if (ev.key === key) setState(ev.value);
1302
+ });
1303
+ }, []);
1304
+ if (!alepha.isBrowser()) {
1305
+ const value = alepha.context.get(key);
1306
+ if (value !== null) return [value, (_) => {}];
1307
+ }
1308
+ return [state, (value) => {
1309
+ alepha.state(key, value);
1310
+ }];
1214
1311
  };
1215
1312
 
1216
1313
  //#endregion
1217
1314
  //#region src/hooks/useClient.ts
1218
1315
  const useClient = (_scope) => {
1316
+ useStore("user");
1219
1317
  return useInject(LinkProvider).client();
1220
1318
  };
1221
1319
 
1222
1320
  //#endregion
1223
1321
  //#region src/hooks/useQueryParams.ts
1224
1322
  const useQueryParams = (schema, options = {}) => {
1225
- const ctx = useContext(RouterContext);
1226
- if (!ctx) throw new Error("useQueryParams must be used within a RouterProvider");
1323
+ const alepha = useAlepha();
1227
1324
  const key = options.key ?? "q";
1228
1325
  const router = useRouter();
1229
1326
  const querystring = router.query[key];
1230
- const [queryParams, setQueryParams] = useState(decode(ctx.alepha, schema, router.query[key]));
1327
+ const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
1231
1328
  useEffect(() => {
1232
- setQueryParams(decode(ctx.alepha, schema, querystring));
1329
+ setQueryParams(decode(alepha, schema, querystring));
1233
1330
  }, [querystring]);
1234
1331
  return [queryParams, (queryParams$1) => {
1235
1332
  setQueryParams(queryParams$1);
1236
1333
  router.setQueryParams((data) => {
1237
1334
  return {
1238
1335
  ...data,
1239
- [key]: encode(ctx.alepha, schema, queryParams$1)
1336
+ [key]: encode(alepha, schema, queryParams$1)
1240
1337
  };
1241
1338
  });
1242
1339
  }];
@@ -1255,14 +1352,50 @@ const decode = (alepha, schema, data) => {
1255
1352
  //#endregion
1256
1353
  //#region src/hooks/useRouterState.ts
1257
1354
  const useRouterState = () => {
1258
- const ctx = useContext(RouterContext);
1355
+ const router = useContext(RouterContext);
1259
1356
  const layer = useContext(RouterLayerContext);
1260
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1261
- const [state, setState] = useState(ctx.state);
1357
+ if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1358
+ const [state, setState] = useState(router.state);
1262
1359
  useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1263
1360
  return state;
1264
1361
  };
1265
1362
 
1363
+ //#endregion
1364
+ //#region src/hooks/useSchema.ts
1365
+ const useSchema = (action) => {
1366
+ const name = action.name;
1367
+ const alepha = useAlepha();
1368
+ const httpClient = useInject(HttpClient);
1369
+ const linkProvider = useInject(LinkProvider);
1370
+ const [schema, setSchema] = useState(ssrSchemaLoading(alepha, name));
1371
+ useEffect(() => {
1372
+ if (!schema.loading) return;
1373
+ const opts = { cache: true };
1374
+ httpClient.fetch(`${linkProvider.URL_LINKS}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1375
+ }, [name]);
1376
+ return schema;
1377
+ };
1378
+ /**
1379
+ * Get an action schema during server-side rendering (SSR) or client-side rendering (CSR).
1380
+ */
1381
+ const ssrSchemaLoading = (alepha, name) => {
1382
+ if (!alepha.isBrowser()) {
1383
+ const links = alepha.context.get("links")?.links ?? [];
1384
+ const can = links.find((it) => it.name === name);
1385
+ if (can) {
1386
+ const schema$1 = alepha.inject(LinkProvider).links?.find((it) => it.name === name)?.schema;
1387
+ if (schema$1) {
1388
+ can.schema = schema$1;
1389
+ return schema$1;
1390
+ }
1391
+ }
1392
+ return { loading: true };
1393
+ }
1394
+ const schema = alepha.inject(LinkProvider).links?.find((it) => it.name === name)?.schema;
1395
+ if (schema) return schema;
1396
+ return { loading: true };
1397
+ };
1398
+
1266
1399
  //#endregion
1267
1400
  //#region src/index.ts
1268
1401
  /**
@@ -1287,5 +1420,5 @@ const AlephaReact = $module({
1287
1420
  });
1288
1421
 
1289
1422
  //#endregion
1290
- export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
1423
+ export { $page, AlephaContext, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, Redirection, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1291
1424
  //# sourceMappingURL=index.js.map