@alepha/react 0.6.1 → 0.6.2

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 { __descriptor, KIND, NotImplementedError, EventEmitter, $logger, $inject, Alepha, $hook } from '@alepha/core';
1
+ import { __descriptor, KIND, NotImplementedError, EventEmitter, $logger, $inject, Alepha, $hook, t } from '@alepha/core';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import React, { createContext, useContext, useState, useEffect, createElement, useMemo } from 'react';
4
4
  import { HttpClient } from '@alepha/server';
@@ -10,7 +10,10 @@ const $auth = (options) => {
10
10
  __descriptor(KEY);
11
11
  return {
12
12
  [KIND]: KEY,
13
- options
13
+ options,
14
+ jwks: () => {
15
+ return options.oidc?.issuer ?? "";
16
+ }
14
17
  };
15
18
  };
16
19
  $auth[KIND] = KEY;
@@ -93,10 +96,7 @@ class Router extends EventEmitter {
93
96
  state,
94
97
  router: this,
95
98
  alepha: this.alepha,
96
- args: {
97
- user: context.user,
98
- cookies: context.cookies
99
- }
99
+ args: context
100
100
  }
101
101
  },
102
102
  state.layers[0]?.element
@@ -263,6 +263,7 @@ class Router extends EventEmitter {
263
263
  });
264
264
  if (prev === curr) {
265
265
  it.props = previous[i].props;
266
+ it.error = previous[i].error;
266
267
  context = {
267
268
  ...context,
268
269
  ...it.props
@@ -305,7 +306,7 @@ class Router extends EventEmitter {
305
306
  for (const key of Object.keys(params2)) {
306
307
  params2[key] = String(params2[key]);
307
308
  }
308
- if (it.route.helmet && renderContext) {
309
+ if (it.route.head && renderContext && !it.error) {
309
310
  this.mergeRenderContext(it.route, renderContext, {
310
311
  ...props,
311
312
  ...context
@@ -316,13 +317,14 @@ class Router extends EventEmitter {
316
317
  const path = acc.replace(/\/+/, "/");
317
318
  if (it.error) {
318
319
  const errorHandler = this.getErrorHandler(it.route);
319
- const element = errorHandler ? errorHandler({
320
+ const element = await (errorHandler ? errorHandler({
320
321
  ...it.config,
321
322
  error: it.error,
322
323
  url
323
- }) : this.renderError(it.error);
324
+ }) : this.renderError(it.error));
324
325
  layers.push({
325
326
  props,
327
+ error: it.error,
326
328
  name: it.route.name,
327
329
  part: it.route.path,
328
330
  config: it.config,
@@ -386,15 +388,32 @@ class Router extends EventEmitter {
386
388
  * @protected
387
389
  */
388
390
  mergeRenderContext(page, ctx, props) {
389
- if (page.helmet) {
390
- const helmet = typeof page.helmet === "function" ? page.helmet(props) : page.helmet;
391
- if (helmet.title) {
392
- ctx.helmet ??= {};
393
- if (ctx.helmet?.title) {
394
- ctx.helmet.title = `${helmet.title} - ${ctx.helmet.title}`;
391
+ if (page.head) {
392
+ ctx.head ??= {};
393
+ const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
394
+ if (head.title) {
395
+ ctx.head ??= {};
396
+ if (ctx.head.titleSeparator) {
397
+ ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
395
398
  } else {
396
- ctx.helmet.title = helmet.title;
399
+ ctx.head.title = head.title;
397
400
  }
401
+ ctx.head.titleSeparator = head.titleSeparator;
402
+ }
403
+ if (head.htmlAttributes) {
404
+ ctx.head.htmlAttributes = {
405
+ ...ctx.head.htmlAttributes,
406
+ ...head.htmlAttributes
407
+ };
408
+ }
409
+ if (head.bodyAttributes) {
410
+ ctx.head.bodyAttributes = {
411
+ ...ctx.head.bodyAttributes,
412
+ ...head.bodyAttributes
413
+ };
414
+ }
415
+ if (head.meta) {
416
+ ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
398
417
  }
399
418
  }
400
419
  }
@@ -563,10 +582,14 @@ class PageDescriptorProvider {
563
582
  }
564
583
  }
565
584
 
585
+ const envSchema = t.object({
586
+ REACT_ROOT_ID: t.string({ default: "root" })
587
+ });
566
588
  class ReactBrowserProvider {
567
589
  log = $logger();
568
590
  client = $inject(HttpClient);
569
591
  router = $inject(Router);
592
+ env = $inject(envSchema);
570
593
  root;
571
594
  transitioning;
572
595
  state = {
@@ -654,12 +677,49 @@ class ReactBrowserProvider {
654
677
  return await this.render({ url: result.redirect });
655
678
  }
656
679
  this.transitioning = void 0;
657
- return { url };
680
+ return { url, context: result.context };
658
681
  }
659
- renderHelmetContext(ctx) {
682
+ /**
683
+ * Render the helmet context.
684
+ *
685
+ * @param ctx
686
+ * @protected
687
+ */
688
+ renderHeadContext(ctx) {
660
689
  if (ctx.title) {
661
690
  this.document.title = ctx.title;
662
691
  }
692
+ if (ctx.bodyAttributes) {
693
+ for (const [key, value] of Object.entries(ctx.bodyAttributes)) {
694
+ if (value) {
695
+ this.document.body.setAttribute(key, value);
696
+ } else {
697
+ this.document.body.removeAttribute(key);
698
+ }
699
+ }
700
+ }
701
+ if (ctx.htmlAttributes) {
702
+ for (const [key, value] of Object.entries(ctx.htmlAttributes)) {
703
+ if (value) {
704
+ this.document.documentElement.setAttribute(key, value);
705
+ } else {
706
+ this.document.documentElement.removeAttribute(key);
707
+ }
708
+ }
709
+ }
710
+ if (ctx.meta) {
711
+ for (const [key, value] of Object.entries(ctx.meta)) {
712
+ const meta = this.document.querySelector(`meta[name="${key}"]`);
713
+ if (meta) {
714
+ meta.setAttribute("content", value.content);
715
+ } else {
716
+ const newMeta = this.document.createElement("meta");
717
+ newMeta.setAttribute("name", key);
718
+ newMeta.setAttribute("content", value.content);
719
+ this.document.head.appendChild(newMeta);
720
+ }
721
+ }
722
+ }
663
723
  }
664
724
  /**
665
725
  * Get embedded layers from the server.
@@ -680,13 +740,13 @@ class ReactBrowserProvider {
680
740
  * @protected
681
741
  */
682
742
  getRootElement() {
683
- const root = this.document.getElementById("root");
743
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
684
744
  if (root) {
685
745
  return root;
686
746
  }
687
747
  const div = this.document.createElement("div");
688
- div.id = "root";
689
- this.document.body.appendChild(div);
748
+ div.id = this.env.REACT_ROOT_ID;
749
+ this.document.body.prepend(div);
690
750
  return div;
691
751
  }
692
752
  getUserFromCookies() {
@@ -711,7 +771,13 @@ class ReactBrowserProvider {
711
771
  handler: async () => {
712
772
  const cache = this.getHydrationState();
713
773
  const previous = cache?.layers ?? [];
714
- await this.render({ previous });
774
+ if (cache?.links) {
775
+ this.client.links = cache.links;
776
+ }
777
+ const { context } = await this.render({ previous });
778
+ if (context.head) {
779
+ this.renderHeadContext(context.head);
780
+ }
715
781
  const element = this.router.root(this.state, {
716
782
  user: cache?.user ?? this.getUserFromCookies()
717
783
  });
@@ -719,40 +785,30 @@ class ReactBrowserProvider {
719
785
  this.root = hydrateRoot(this.getRootElement(), element);
720
786
  this.log.info("Hydrated root element");
721
787
  } else {
722
- this.root = createRoot(this.getRootElement());
788
+ this.root ??= createRoot(this.getRootElement());
723
789
  this.root.render(element);
724
790
  this.log.info("Created root element");
725
791
  }
726
792
  window.addEventListener("popstate", () => {
727
793
  this.render();
728
794
  });
729
- this.router.on("end", ({ context }) => {
730
- if (context.helmet) {
731
- this.renderHelmetContext(context.helmet);
795
+ this.router.on("end", ({ context: context2 }) => {
796
+ if (context2.head) {
797
+ this.renderHeadContext(context2.head);
732
798
  }
733
799
  });
734
800
  }
735
801
  });
736
- /**
737
- *
738
- * @protected
739
- */
740
- stop = $hook({
741
- name: "stop",
742
- handler: async () => {
743
- if (this.root) {
744
- this.root.unmount();
745
- this.log.info("Unmounted root element");
746
- }
747
- }
748
- });
749
802
  }
750
803
 
751
804
  class Auth {
752
805
  alepha = $inject(Alepha);
753
806
  log = $logger();
754
807
  client = $inject(HttpClient);
755
- api = "/api/_oauth/login";
808
+ slugs = {
809
+ login: "/api/_oauth/login",
810
+ logout: "/api/_oauth/logout"
811
+ };
756
812
  start = $hook({
757
813
  name: "start",
758
814
  handler: async () => {
@@ -767,16 +823,16 @@ class Auth {
767
823
  if (this.alepha.isBrowser()) {
768
824
  const browser = this.alepha.get(ReactBrowserProvider);
769
825
  const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
770
- window.location.href = `${this.api}?redirect=${redirect}`;
826
+ window.location.href = `${this.slugs.login}?redirect=${redirect}`;
771
827
  if (browser.transitioning) {
772
828
  throw new RedirectionError(browser.state.pathname);
773
829
  }
774
830
  return;
775
831
  }
776
- throw new RedirectionError(this.api);
832
+ throw new RedirectionError(this.slugs.login);
777
833
  };
778
834
  logout = () => {
779
- window.location.href = `/api/_oauth/logout?redirect=${encodeURIComponent(window.location.origin)}`;
835
+ window.location.href = `${this.slugs.logout}?redirect=${encodeURIComponent(window.location.origin)}`;
780
836
  };
781
837
  }
782
838
 
@@ -906,8 +962,18 @@ const useRouter = () => {
906
962
 
907
963
  const Link = (props) => {
908
964
  React.useContext(RouterContext);
965
+ const to = typeof props.to === "string" ? props.to : props.to.options.path;
966
+ if (!to) {
967
+ return null;
968
+ }
969
+ const can = typeof props.to === "string" ? void 0 : props.to.options.can;
970
+ if (can && !can()) {
971
+ console.log("I cannot go to", to);
972
+ return null;
973
+ }
974
+ const name = typeof props.to === "string" ? void 0 : props.to.options.name;
909
975
  const router = useRouter();
910
- return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(props.to), ...props, children: props.children });
976
+ return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(to), ...props, children: props.children ?? name });
911
977
  };
912
978
 
913
979
  const useInject = (clazz) => {
@@ -915,7 +981,9 @@ const useInject = (clazz) => {
915
981
  if (!ctx) {
916
982
  throw new Error("useRouter must be used within a <RouterProvider>");
917
983
  }
918
- return ctx.alepha.get(clazz);
984
+ return ctx.alepha.get(clazz, {
985
+ skipRegistration: true
986
+ });
919
987
  };
920
988
 
921
989
  const useClient = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -9,23 +9,24 @@
9
9
  "./dist/index.js": "./dist/index.browser.js"
10
10
  },
11
11
  "dependencies": {
12
- "@alepha/core": "0.6.1",
13
- "@alepha/security": "0.6.1",
14
- "@alepha/server": "0.6.1",
12
+ "@alepha/core": "0.6.2",
13
+ "@alepha/security": "0.6.2",
14
+ "@alepha/server": "0.6.2",
15
+ "cheerio": "^1.0.0",
15
16
  "openid-client": "^6.4.2",
16
17
  "path-to-regexp": "^8.2.0",
17
- "react-dom": "^18.3.1"
18
+ "react-dom": "^19.1.0"
18
19
  },
19
20
  "devDependencies": {
20
- "@types/react": "^18.3.20",
21
- "@types/react-dom": "^18.3.6",
22
- "pkgroll": "^2.12.1",
23
- "react": "^18.3.1",
24
- "vitest": "^3.1.1"
21
+ "@types/react": "^19.1.2",
22
+ "@types/react-dom": "^19.1.3",
23
+ "pkgroll": "^2.12.2",
24
+ "react": "^19.1.0",
25
+ "vitest": "^3.1.2"
25
26
  },
26
27
  "peerDependencies": {
27
- "@types/react": "^18",
28
- "react": "^18"
28
+ "@types/react": "^19",
29
+ "react": "^19"
29
30
  },
30
31
  "scripts": {
31
32
  "build": "pkgroll --clean-dist"