@alepha/react 0.5.1 → 0.6.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.
@@ -1,11 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@alepha/core');
4
- var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var React = require('react');
5
6
  var server = require('@alepha/server');
6
7
  var client = require('react-dom/client');
7
8
  var pathToRegexp = require('path-to-regexp');
8
9
 
10
+ const KEY = "AUTH";
11
+ const $auth = (options) => {
12
+ core.__descriptor(KEY);
13
+ return {
14
+ [core.KIND]: KEY,
15
+ options
16
+ };
17
+ };
18
+ $auth[core.KIND] = KEY;
19
+
9
20
  const pageDescriptorKey = "PAGE";
10
21
  const $page = (options) => {
11
22
  core.__descriptor(pageDescriptorKey);
@@ -25,35 +36,36 @@ const $page = (options) => {
25
36
  };
26
37
  $page[core.KIND] = pageDescriptorKey;
27
38
 
28
- const RouterContext = react.createContext(
39
+ const RouterContext = React.createContext(
29
40
  void 0
30
41
  );
31
42
 
32
- const RouterLayerContext = react.createContext(void 0);
43
+ const RouterLayerContext = React.createContext(void 0);
33
44
 
34
45
  const NestedView = (props) => {
35
- const app = react.useContext(RouterContext);
36
- const layer = react.useContext(RouterLayerContext);
46
+ const app = React.useContext(RouterContext);
47
+ const layer = React.useContext(RouterLayerContext);
37
48
  const index = layer?.index ?? 0;
38
- const [view, setView] = react.useState(
49
+ const [view, setView] = React.useState(
39
50
  app?.state.layers[index]?.element
40
51
  );
41
- react.useEffect(() => {
52
+ React.useEffect(() => {
42
53
  if (app?.alepha.isBrowser()) {
43
- return app?.router.on("end", ({ layers }) => {
44
- setView(layers[index]?.element);
54
+ return app?.router.on("end", (state) => {
55
+ setView(state.layers[index]?.element);
45
56
  });
46
57
  }
47
58
  }, [app]);
48
59
  return view ?? props.children ?? null;
49
60
  };
50
61
 
51
- class RedirectException extends Error {
62
+ class RedirectionError extends Error {
52
63
  constructor(page) {
53
64
  super("Redirection");
54
65
  this.page = page;
55
66
  }
56
67
  }
68
+
57
69
  class Router extends core.EventEmitter {
58
70
  log = core.$logger();
59
71
  alepha = core.$inject(core.Alepha);
@@ -75,15 +87,18 @@ class Router extends core.EventEmitter {
75
87
  /**
76
88
  *
77
89
  */
78
- root(state, opts = {}) {
79
- return react.createElement(
90
+ root(state, context = {}) {
91
+ return React.createElement(
80
92
  RouterContext.Provider,
81
93
  {
82
94
  value: {
83
95
  state,
84
96
  router: this,
85
97
  alepha: this.alepha,
86
- session: opts.user ? { user: opts.user } : void 0
98
+ args: {
99
+ user: context.user,
100
+ cookies: context.cookies
101
+ }
87
102
  }
88
103
  },
89
104
  state.layers[0]?.element
@@ -99,11 +114,12 @@ class Router extends core.EventEmitter {
99
114
  const state = {
100
115
  pathname,
101
116
  search,
102
- layers: []
117
+ layers: [],
118
+ context: {}
103
119
  };
104
- this.emit("begin", void 0);
120
+ await this.emit("begin", void 0);
105
121
  try {
106
- let layers = await this.match(url, options);
122
+ let layers = await this.match(url, options, state.context);
107
123
  if (layers.length === 0) {
108
124
  if (this.notFoundPageRoute) {
109
125
  layers = await this.createLayers(url, this.notFoundPageRoute);
@@ -117,13 +133,14 @@ class Router extends core.EventEmitter {
117
133
  }
118
134
  }
119
135
  state.layers = layers;
120
- this.emit("success", void 0);
136
+ await this.emit("success", void 0);
121
137
  } catch (e) {
122
- if (e instanceof RedirectException) {
138
+ if (e instanceof RedirectionError) {
123
139
  return {
124
140
  element: null,
125
141
  layers: [],
126
- redirect: typeof e.page === "string" ? e.page : this.href(e.page)
142
+ redirect: typeof e.page === "string" ? e.page : this.href(e.page),
143
+ context: state.context
127
144
  };
128
145
  }
129
146
  this.log.error(e);
@@ -135,31 +152,35 @@ class Router extends core.EventEmitter {
135
152
  path: "/"
136
153
  }
137
154
  ];
138
- this.emit("error", e);
155
+ await this.emit("error", e);
139
156
  }
140
157
  if (options.state) {
141
158
  options.state.layers = state.layers;
142
159
  options.state.pathname = state.pathname;
143
160
  options.state.search = state.search;
144
- this.emit("end", options.state);
161
+ options.state.context = state.context;
162
+ await this.emit("end", options.state);
145
163
  return {
146
- element: this.root(options.state, options),
147
- layers: options.state.layers
164
+ element: this.root(options.state, options.args),
165
+ layers: options.state.layers,
166
+ context: state.context
148
167
  };
149
168
  }
150
- this.emit("end", state);
169
+ await this.emit("end", state);
151
170
  return {
152
- element: this.root(state, options),
153
- layers: state.layers
171
+ element: this.root(state, options.args),
172
+ layers: state.layers,
173
+ context: state.context
154
174
  };
155
175
  }
156
176
  /**
157
177
  *
158
178
  * @param url
159
179
  * @param options
180
+ * @param context
160
181
  * @protected
161
182
  */
162
- async match(url, options = {}) {
183
+ async match(url, options = {}, context = {}) {
163
184
  const pages = this.pages;
164
185
  const previous = options.previous;
165
186
  const [pathname, search] = url.split("?");
@@ -181,7 +202,8 @@ class Router extends core.EventEmitter {
181
202
  params,
182
203
  query,
183
204
  previous,
184
- options.user
205
+ options.args,
206
+ context
185
207
  );
186
208
  }
187
209
  }
@@ -190,14 +212,16 @@ class Router extends core.EventEmitter {
190
212
  /**
191
213
  * Create layers for the given route.
192
214
  *
215
+ * @param url
193
216
  * @param route
194
217
  * @param params
195
218
  * @param query
196
219
  * @param previous
197
- * @param user
220
+ * @param args
221
+ * @param renderContext
198
222
  * @protected
199
223
  */
200
- async createLayers(url, route, params = {}, query = {}, previous = [], user) {
224
+ async createLayers(url, route, params = {}, query = {}, previous = [], args, renderContext) {
201
225
  const layers = [];
202
226
  let context = {};
203
227
  const stack = [{ route }];
@@ -212,13 +236,13 @@ class Router extends core.EventEmitter {
212
236
  const route2 = it.route;
213
237
  const config = {};
214
238
  try {
215
- config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : {};
239
+ config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : query;
216
240
  } catch (e) {
217
241
  it.error = e;
218
242
  break;
219
243
  }
220
244
  try {
221
- config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : {};
245
+ config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : params;
222
246
  } catch (e) {
223
247
  it.error = e;
224
248
  break;
@@ -250,12 +274,15 @@ class Router extends core.EventEmitter {
250
274
  forceRefresh = true;
251
275
  }
252
276
  try {
253
- const props = await route2.resolve?.({
254
- ...config,
255
- ...context,
256
- user,
257
- url
258
- }) ?? {};
277
+ const props = await route2.resolve?.(
278
+ {
279
+ ...config,
280
+ ...context,
281
+ context: args,
282
+ url
283
+ },
284
+ args ?? {}
285
+ ) ?? {};
259
286
  it.props = {
260
287
  ...props
261
288
  };
@@ -264,7 +291,7 @@ class Router extends core.EventEmitter {
264
291
  ...props
265
292
  };
266
293
  } catch (e) {
267
- if (e instanceof RedirectException) {
294
+ if (e instanceof RedirectionError) {
268
295
  throw e;
269
296
  }
270
297
  this.log.error(e);
@@ -280,6 +307,12 @@ class Router extends core.EventEmitter {
280
307
  for (const key of Object.keys(params2)) {
281
308
  params2[key] = String(params2[key]);
282
309
  }
310
+ if (it.route.helmet && renderContext) {
311
+ this.mergeRenderContext(it.route, renderContext, {
312
+ ...props,
313
+ ...context
314
+ });
315
+ }
283
316
  acc += "/";
284
317
  acc += it.route.path ? pathToRegexp.compile(it.route.path)(params2) : "";
285
318
  const path = acc.replace(/\/+/, "/");
@@ -339,20 +372,41 @@ class Router extends core.EventEmitter {
339
372
  async createElement(page, props) {
340
373
  if (page.lazy) {
341
374
  const component = await page.lazy();
342
- return react.createElement(component.default, props);
375
+ return React.createElement(component.default, props);
343
376
  }
344
377
  if (page.component) {
345
- return react.createElement(page.component, props);
378
+ return React.createElement(page.component, props);
346
379
  }
347
380
  return void 0;
348
381
  }
382
+ /**
383
+ * Merge the render context with the page context.
384
+ *
385
+ * @param page
386
+ * @param ctx
387
+ * @param props
388
+ * @protected
389
+ */
390
+ mergeRenderContext(page, ctx, props) {
391
+ if (page.helmet) {
392
+ const helmet = typeof page.helmet === "function" ? page.helmet(props) : page.helmet;
393
+ if (helmet.title) {
394
+ ctx.helmet ??= {};
395
+ if (ctx.helmet?.title) {
396
+ ctx.helmet.title = `${helmet.title} - ${ctx.helmet.title}`;
397
+ } else {
398
+ ctx.helmet.title = helmet.title;
399
+ }
400
+ }
401
+ }
402
+ }
349
403
  /**
350
404
  *
351
405
  * @param e
352
406
  * @protected
353
407
  */
354
408
  renderError(e) {
355
- return react.createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
409
+ return React.createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
356
410
  }
357
411
  /**
358
412
  * Render an empty view.
@@ -360,7 +414,7 @@ class Router extends core.EventEmitter {
360
414
  * @protected
361
415
  */
362
416
  renderEmptyView() {
363
- return react.createElement(NestedView, {});
417
+ return React.createElement(NestedView, {});
364
418
  }
365
419
  /**
366
420
  * Create a valid href for the given page.
@@ -389,7 +443,7 @@ class Router extends core.EventEmitter {
389
443
  * @protected
390
444
  */
391
445
  renderView(index, path, view = this.renderEmptyView()) {
392
- return react.createElement(
446
+ return React.createElement(
393
447
  RouterLayerContext.Provider,
394
448
  {
395
449
  value: {
@@ -517,7 +571,12 @@ class ReactBrowserProvider {
517
571
  router = core.$inject(Router);
518
572
  root;
519
573
  transitioning;
520
- state = { layers: [], pathname: "", search: "" };
574
+ state = {
575
+ layers: [],
576
+ pathname: "",
577
+ search: "",
578
+ context: {}
579
+ };
521
580
  /**
522
581
  *
523
582
  */
@@ -599,12 +658,17 @@ class ReactBrowserProvider {
599
658
  this.transitioning = void 0;
600
659
  return { url };
601
660
  }
661
+ renderHelmetContext(ctx) {
662
+ if (ctx.title) {
663
+ this.document.title = ctx.title;
664
+ }
665
+ }
602
666
  /**
603
667
  * Get embedded layers from the server.
604
668
  *
605
669
  * @protected
606
670
  */
607
- getEmbeddedCache() {
671
+ getHydrationState() {
608
672
  try {
609
673
  if ("__ssr" in window && typeof window.__ssr === "object") {
610
674
  return window.__ssr;
@@ -627,6 +691,18 @@ class ReactBrowserProvider {
627
691
  this.document.body.appendChild(div);
628
692
  return div;
629
693
  }
694
+ getUserFromCookies() {
695
+ const cookies = this.document.cookie.split("; ");
696
+ const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
697
+ try {
698
+ if (userCookie) {
699
+ return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
700
+ }
701
+ } catch (error) {
702
+ this.log.warn(error, "Failed to parse user cookie");
703
+ }
704
+ return void 0;
705
+ }
630
706
  // -------------------------------------------------------------------------------------------------------------------
631
707
  /**
632
708
  *
@@ -635,11 +711,12 @@ class ReactBrowserProvider {
635
711
  ready = core.$hook({
636
712
  name: "ready",
637
713
  handler: async () => {
638
- const cache = this.getEmbeddedCache();
714
+ const cache = this.getHydrationState();
639
715
  const previous = cache?.layers ?? [];
640
- const session = cache?.session ?? await this.client.of().session();
641
716
  await this.render({ previous });
642
- const element = this.router.root(this.state, session);
717
+ const element = this.router.root(this.state, {
718
+ user: cache?.user ?? this.getUserFromCookies()
719
+ });
643
720
  if (previous.length > 0) {
644
721
  this.root = client.hydrateRoot(this.getRootElement(), element);
645
722
  this.log.info("Hydrated root element");
@@ -651,6 +728,11 @@ class ReactBrowserProvider {
651
728
  window.addEventListener("popstate", () => {
652
729
  this.render();
653
730
  });
731
+ this.router.on("end", ({ context }) => {
732
+ if (context.helmet) {
733
+ this.renderHelmetContext(context.helmet);
734
+ }
735
+ });
654
736
  }
655
737
  });
656
738
  /**
@@ -668,6 +750,38 @@ class ReactBrowserProvider {
668
750
  });
669
751
  }
670
752
 
753
+ class Auth {
754
+ alepha = core.$inject(core.Alepha);
755
+ log = core.$logger();
756
+ client = core.$inject(server.HttpClient);
757
+ api = "/api/_oauth/login";
758
+ start = core.$hook({
759
+ name: "start",
760
+ handler: async () => {
761
+ this.client.on("onError", (err) => {
762
+ if (err.statusCode === 401) {
763
+ this.login();
764
+ }
765
+ });
766
+ }
767
+ });
768
+ login = (provider) => {
769
+ if (this.alepha.isBrowser()) {
770
+ const browser = this.alepha.get(ReactBrowserProvider);
771
+ const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
772
+ window.location.href = `${this.api}?redirect=${redirect}`;
773
+ if (browser.transitioning) {
774
+ throw new RedirectionError(browser.state.pathname);
775
+ }
776
+ return;
777
+ }
778
+ throw new RedirectionError(this.api);
779
+ };
780
+ logout = () => {
781
+ window.location.href = `/api/_oauth/logout?redirect=${encodeURIComponent(window.location.origin)}`;
782
+ };
783
+ }
784
+
671
785
  class RouterHookApi {
672
786
  constructor(state, layer, browser) {
673
787
  this.state = state;
@@ -777,12 +891,12 @@ class RouterHookApi {
777
891
  }
778
892
 
779
893
  const useRouter = () => {
780
- const ctx = react.useContext(RouterContext);
781
- const layer = react.useContext(RouterLayerContext);
894
+ const ctx = React.useContext(RouterContext);
895
+ const layer = React.useContext(RouterLayerContext);
782
896
  if (!ctx || !layer) {
783
897
  throw new Error("useRouter must be used within a RouterProvider");
784
898
  }
785
- return react.useMemo(
899
+ return React.useMemo(
786
900
  () => new RouterHookApi(
787
901
  ctx.state,
788
902
  layer,
@@ -792,51 +906,18 @@ const useRouter = () => {
792
906
  );
793
907
  };
794
908
 
795
- const useActive = (path) => {
909
+ const Link = (props) => {
910
+ React.useContext(RouterContext);
796
911
  const router = useRouter();
797
- const ctx = react.useContext(RouterContext);
798
- const layer = react.useContext(RouterLayerContext);
799
- if (!ctx || !layer) {
800
- throw new Error("useRouter must be used within a RouterProvider");
801
- }
802
- let name;
803
- if (typeof path === "object" && path.options.name) {
804
- name = path.options.name;
805
- }
806
- const [current, setCurrent] = react.useState(ctx.state.pathname);
807
- const href = react.useMemo(() => router.createHref(path, layer), [path, layer]);
808
- const [isPending, setPending] = react.useState(false);
809
- const isActive = current === href;
810
- react.useEffect(
811
- () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
812
- []
813
- );
814
- return {
815
- name,
816
- isPending,
817
- isActive,
818
- anchorProps: {
819
- href,
820
- onClick: (ev) => {
821
- ev.stopPropagation();
822
- ev.preventDefault();
823
- if (isActive) return;
824
- if (isPending) return;
825
- setPending(true);
826
- router.go(href).then(() => {
827
- setPending(false);
828
- });
829
- }
830
- }
831
- };
912
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { ...router.createAnchorProps(props.to), ...props, children: props.children });
832
913
  };
833
914
 
834
- const useInject = (classEntry) => {
835
- const ctx = react.useContext(RouterContext);
915
+ const useInject = (clazz) => {
916
+ const ctx = React.useContext(RouterContext);
836
917
  if (!ctx) {
837
918
  throw new Error("useRouter must be used within a <RouterProvider>");
838
919
  }
839
- return ctx.alepha.get(classEntry);
920
+ return ctx.alepha.get(clazz);
840
921
  };
841
922
 
842
923
  const useClient = () => {
@@ -844,17 +925,17 @@ const useClient = () => {
844
925
  };
845
926
 
846
927
  const useQueryParams = (schema, options = {}) => {
847
- const ctx = react.useContext(RouterContext);
928
+ const ctx = React.useContext(RouterContext);
848
929
  if (!ctx) {
849
930
  throw new Error("useQueryParams must be used within a RouterProvider");
850
931
  }
851
932
  const key = options.key ?? "q";
852
933
  const router = useRouter();
853
934
  const querystring = router.query[key];
854
- const [queryParams, setQueryParams] = react.useState(
935
+ const [queryParams, setQueryParams] = React.useState(
855
936
  decode(ctx.alepha, schema, router.query[key])
856
937
  );
857
- react.useEffect(() => {
938
+ React.useEffect(() => {
858
939
  setQueryParams(decode(ctx.alepha, schema, querystring));
859
940
  }, [querystring]);
860
941
  return [
@@ -882,12 +963,12 @@ const decode = (alepha, schema, data) => {
882
963
  };
883
964
 
884
965
  const useRouterEvents = (opts = {}) => {
885
- const ctx = react.useContext(RouterContext);
886
- const layer = react.useContext(RouterLayerContext);
966
+ const ctx = React.useContext(RouterContext);
967
+ const layer = React.useContext(RouterLayerContext);
887
968
  if (!ctx || !layer) {
888
969
  throw new Error("useRouter must be used within a RouterProvider");
889
970
  }
890
- react.useEffect(() => {
971
+ React.useEffect(() => {
891
972
  const subs = [];
892
973
  const onBegin = opts.onBegin;
893
974
  const onEnd = opts.onEnd;
@@ -910,13 +991,13 @@ const useRouterEvents = (opts = {}) => {
910
991
  };
911
992
 
912
993
  const useRouterState = () => {
913
- const ctx = react.useContext(RouterContext);
914
- const layer = react.useContext(RouterLayerContext);
994
+ const ctx = React.useContext(RouterContext);
995
+ const layer = React.useContext(RouterLayerContext);
915
996
  if (!ctx || !layer) {
916
997
  throw new Error("useRouter must be used within a RouterProvider");
917
998
  }
918
- const [state, setState] = react.useState(ctx.state);
919
- react.useEffect(
999
+ const [state, setState] = React.useState(ctx.state);
1000
+ React.useEffect(
920
1001
  () => ctx.router.on("end", (it) => {
921
1002
  setState({ ...it });
922
1003
  }),
@@ -925,17 +1006,77 @@ const useRouterState = () => {
925
1006
  return state;
926
1007
  };
927
1008
 
1009
+ const useActive = (path) => {
1010
+ const router = useRouter();
1011
+ const ctx = React.useContext(RouterContext);
1012
+ const layer = React.useContext(RouterLayerContext);
1013
+ if (!ctx || !layer) {
1014
+ throw new Error("useRouter must be used within a RouterProvider");
1015
+ }
1016
+ let name;
1017
+ if (typeof path === "object" && path.options.name) {
1018
+ name = path.options.name;
1019
+ }
1020
+ const [current, setCurrent] = React.useState(ctx.state.pathname);
1021
+ const href = React.useMemo(() => router.createHref(path, layer), [path, layer]);
1022
+ const [isPending, setPending] = React.useState(false);
1023
+ const isActive = current === href;
1024
+ React.useEffect(
1025
+ () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
1026
+ []
1027
+ );
1028
+ return {
1029
+ name,
1030
+ isPending,
1031
+ isActive,
1032
+ anchorProps: {
1033
+ href,
1034
+ onClick: (ev) => {
1035
+ ev.stopPropagation();
1036
+ ev.preventDefault();
1037
+ if (isActive) return;
1038
+ if (isPending) return;
1039
+ setPending(true);
1040
+ router.go(href).then(() => {
1041
+ setPending(false);
1042
+ });
1043
+ }
1044
+ }
1045
+ };
1046
+ };
1047
+
1048
+ const useAuth = () => {
1049
+ const ctx = React.useContext(RouterContext);
1050
+ if (!ctx) {
1051
+ throw new Error("useAuth must be used within a RouterContext");
1052
+ }
1053
+ const args = ctx.args ?? {};
1054
+ return {
1055
+ user: args.user,
1056
+ logout: () => {
1057
+ ctx.alepha.get(Auth).logout();
1058
+ },
1059
+ login: (provider) => {
1060
+ ctx.alepha.get(Auth).login();
1061
+ }
1062
+ };
1063
+ };
1064
+
1065
+ exports.$auth = $auth;
928
1066
  exports.$page = $page;
1067
+ exports.Auth = Auth;
1068
+ exports.Link = Link;
929
1069
  exports.NestedView = NestedView;
930
1070
  exports.PageDescriptorProvider = PageDescriptorProvider;
931
1071
  exports.ReactBrowserProvider = ReactBrowserProvider;
932
- exports.RedirectException = RedirectException;
1072
+ exports.RedirectionError = RedirectionError;
933
1073
  exports.Router = Router;
934
1074
  exports.RouterContext = RouterContext;
935
1075
  exports.RouterHookApi = RouterHookApi;
936
1076
  exports.RouterLayerContext = RouterLayerContext;
937
1077
  exports.pageDescriptorKey = pageDescriptorKey;
938
1078
  exports.useActive = useActive;
1079
+ exports.useAuth = useAuth;
939
1080
  exports.useClient = useClient;
940
1081
  exports.useInject = useInject;
941
1082
  exports.useQueryParams = useQueryParams;