@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.
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { $logger, $inject, Alepha, t, $hook, autoInject } from '@alepha/core';
2
- import { FastifyCookieProvider, $cookie, $route, BadRequestError, ServerProvider, ServerModule, ServerLinksProvider } from '@alepha/server';
3
- import { $ as $auth, R as Router, a as $page, A as Auth, P as PageDescriptorProvider } from './useAuth-i7wbKVrt.js';
4
- export { L as Link, N as NestedView, l as ReactBrowserProvider, m as RedirectionError, b as RouterContext, d as RouterHookApi, c as RouterLayerContext, p as pageDescriptorKey, j as useActive, k as useAuth, e as useClient, u as useInject, f as useQueryParams, g as useRouter, h as useRouterEvents, i as useRouterState } from './useAuth-i7wbKVrt.js';
2
+ import { ServerCookieProvider, $cookie, $route, BadRequestError, ServerProvider, ServerLinksProvider, ServerModule } from '@alepha/server';
3
+ import { $ as $auth, R as Router, a as $page, A as Auth, P as PageDescriptorProvider } from './useAuth-Ps01oe8e.js';
4
+ export { L as Link, N as NestedView, l as ReactBrowserProvider, m as RedirectionError, b as RouterContext, d as RouterHookApi, c as RouterLayerContext, p as pageDescriptorKey, j as useActive, k as useAuth, e as useClient, u as useInject, f as useQueryParams, g as useRouter, h as useRouterEvents, i as useRouterState } from './useAuth-Ps01oe8e.js';
5
5
  import { discovery, allowInsecureRequests, refreshTokenGrant, randomPKCECodeVerifier, calculatePKCECodeChallenge, buildAuthorizationUrl, authorizationCodeGrant, buildEndSessionUrl } from 'openid-client';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { readFile } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
+ import { load } from 'cheerio';
9
10
  import { renderToString } from 'react-dom/server';
10
11
  import 'react/jsx-runtime';
11
12
  import 'react';
@@ -15,7 +16,7 @@ import 'path-to-regexp';
15
16
  class ReactAuthProvider {
16
17
  log = $logger();
17
18
  alepha = $inject(Alepha);
18
- fastifyCookieProvider = $inject(FastifyCookieProvider);
19
+ serverCookieProvider = $inject(ServerCookieProvider);
19
20
  authProviders = [];
20
21
  authorizationCode = $cookie({
21
22
  name: "authorizationCode",
@@ -53,23 +54,30 @@ class ReactAuthProvider {
53
54
  name: "configure",
54
55
  handler: async () => {
55
56
  const auths = this.alepha.getDescriptorValues($auth);
56
- for (const auth of auths) {
57
- const options = auth.value.options;
57
+ for (const { value, key, instance } of auths) {
58
+ const options = value.options;
58
59
  if (options.oidc) {
60
+ this.log.debug(
61
+ `Discover OIDC auth provider -> ${options.oidc.issuer}`
62
+ );
63
+ const client = await discovery(
64
+ new URL(options.oidc.issuer),
65
+ options.oidc.clientId,
66
+ {
67
+ client_secret: options.oidc.clientSecret
68
+ },
69
+ void 0,
70
+ {
71
+ execute: [allowInsecureRequests]
72
+ }
73
+ );
74
+ instance[key].jwks = () => {
75
+ return client.serverMetadata().jwks_uri;
76
+ };
59
77
  this.authProviders.push({
60
- name: options.name ?? auth.key,
78
+ name: options.name ?? key,
61
79
  redirectUri: options.oidc.redirectUri ?? "/api/_oauth/callback",
62
- client: await discovery(
63
- new URL(options.oidc.issuer),
64
- options.oidc.clientId,
65
- {
66
- client_secret: options.oidc.clientSecret
67
- },
68
- void 0,
69
- {
70
- execute: [allowInsecureRequests]
71
- }
72
- )
80
+ client
73
81
  });
74
82
  }
75
83
  }
@@ -78,21 +86,19 @@ class ReactAuthProvider {
78
86
  /**
79
87
  * Configure Fastify to forward Session Access Token to Header Authorization.
80
88
  */
81
- configureFastify = $hook({
82
- name: "configure:fastify",
83
- after: this.fastifyCookieProvider,
84
- handler: async (app) => {
85
- app.addHook("onRequest", async (req) => {
86
- if (req.cookies && !this.isViteFile(req.url) && !!this.authProviders.length) {
87
- const tokens = await this.refresh(req.cookies);
88
- if (tokens) {
89
- req.headers.authorization = `Bearer ${tokens.access_token}`;
90
- }
91
- if (this.user.get(req.cookies) && !this.tokens.get(req.cookies)) {
92
- this.user.del(req.cookies);
93
- }
89
+ onRequest = $hook({
90
+ name: "server:onRequest",
91
+ after: this.serverCookieProvider,
92
+ handler: async ({ request }) => {
93
+ if (request.cookies && !this.isViteFile(request.url.pathname) && !!this.authProviders.length) {
94
+ const tokens = await this.refresh(request.cookies);
95
+ if (tokens) {
96
+ request.headers.rep("authorization", `Bearer ${tokens.access_token}`);
94
97
  }
95
- });
98
+ if (this.user.get(request.cookies) && !this.tokens.get(request.cookies)) {
99
+ this.user.del(request.cookies);
100
+ }
101
+ }
96
102
  }
97
103
  });
98
104
  /**
@@ -121,7 +127,9 @@ class ReactAuthProvider {
121
127
  });
122
128
  return newTokens;
123
129
  } catch (e) {
124
- this.log.warn(e, "Failed to refresh token -");
130
+ if (e instanceof Error) {
131
+ this.log.warn("Failed to refresh token", e.message);
132
+ }
125
133
  }
126
134
  }
127
135
  this.tokens.del(cookies);
@@ -315,13 +323,13 @@ const envSchema$1 = t.object({
315
323
  REACT_SERVER_DIST: t.string({ default: "client" }),
316
324
  REACT_SERVER_PREFIX: t.string({ default: "" }),
317
325
  REACT_SSR_ENABLED: t.boolean({ default: false }),
318
- REACT_SSR_OUTLET: t.string({ default: "<!--ssr-outlet-->" })
326
+ REACT_ROOT_ID: t.string({ default: "root" })
319
327
  });
320
328
  class ReactServerProvider {
321
329
  log = $logger();
322
330
  alepha = $inject(Alepha);
323
331
  router = $inject(Router);
324
- server = $inject(ServerProvider);
332
+ serverProvider = $inject(ServerProvider);
325
333
  env = $inject(envSchema$1);
326
334
  configure = $hook({
327
335
  name: "configure",
@@ -329,7 +337,9 @@ class ReactServerProvider {
329
337
  await this.configureRoutes();
330
338
  }
331
339
  });
340
+ id = Math.random().toString(36).substring(2, 7);
332
341
  async configureRoutes() {
342
+ this.alepha.state("ReactServerProvider.ssr", false);
333
343
  if (this.alepha.isTest()) {
334
344
  this.processDescriptors();
335
345
  }
@@ -337,14 +347,15 @@ class ReactServerProvider {
337
347
  return;
338
348
  }
339
349
  if (process.env.VITE_ALEPHA_DEV === "true") {
350
+ const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
340
351
  this.log.info("SSR (vite) OK");
341
- const templateUrl = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}/index.html`;
342
- this.log.debug(`Fetch template from ${templateUrl}`);
352
+ this.alepha.state("ReactServerProvider.ssr", true);
353
+ const templateUrl = `${url}/index.html`;
343
354
  const route2 = this.createHandler(
344
- () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0).then((it) => it ? this.checkTemplate(it) : void 0)
355
+ () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
345
356
  );
346
- await this.server.route(route2);
347
- await this.server.route({
357
+ await this.serverProvider.route(route2);
358
+ await this.serverProvider.route({
348
359
  ...route2,
349
360
  url: "*"
350
361
  });
@@ -366,37 +377,16 @@ class ReactServerProvider {
366
377
  this.log.warn("Missing static files, SSR will be disabled");
367
378
  return;
368
379
  }
369
- await this.server.serve(this.createStaticHandler(root));
380
+ await this.serverProvider.serve(this.createStaticHandler(root));
370
381
  }
371
- const template = this.checkTemplate(
372
- this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8")
373
- );
382
+ const template = this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8");
374
383
  const route = this.createHandler(async () => template);
375
- await this.server.route(route);
376
- await this.server.route({
384
+ await this.serverProvider.route(route);
385
+ await this.serverProvider.route({
377
386
  ...route,
378
387
  url: "*"
379
388
  });
380
- }
381
- /**
382
- * Check if the template contains the outlet.
383
- *
384
- * @param template
385
- * @protected
386
- */
387
- checkTemplate(template) {
388
- if (!template.includes(this.env.REACT_SSR_OUTLET)) {
389
- if (!template.includes('<div id="root"></div>')) {
390
- throw new Error(
391
- `Missing React SSR outlet in index.html, please add ${this.env.REACT_SSR_OUTLET} to the index.html file`
392
- );
393
- }
394
- return template.replace(
395
- `<div id="root"></div>`,
396
- `<div id="root">${this.env.REACT_SSR_OUTLET}</div>`
397
- );
398
- }
399
- return template;
389
+ this.alepha.state("ReactServerProvider.ssr", true);
400
390
  }
401
391
  /**
402
392
  *
@@ -437,6 +427,10 @@ class ReactServerProvider {
437
427
  }
438
428
  };
439
429
  }
430
+ /**
431
+ *
432
+ * @protected
433
+ */
440
434
  processDescriptors() {
441
435
  const pages = this.alepha.getDescriptorValues($page);
442
436
  for (const { key, instance, value } of pages) {
@@ -477,7 +471,20 @@ class ReactServerProvider {
477
471
  * @param template
478
472
  * @param args
479
473
  */
480
- async ssr(url, template = this.env.REACT_SSR_OUTLET, args = {}) {
474
+ async ssr(url, template, args = {}) {
475
+ const hasAuth = this.alepha.has(ReactAuthProvider);
476
+ if (!args.user && args.cookies && hasAuth) {
477
+ const auth = this.alepha.get(ReactAuthProvider);
478
+ args.user = auth.user.get(args.cookies);
479
+ if (args.user) {
480
+ args.user.roles = [];
481
+ }
482
+ }
483
+ if (this.alepha.has(ServerLinksProvider) && hasAuth) {
484
+ const srv = this.alepha.get(ServerLinksProvider);
485
+ args.links = await srv.links();
486
+ this.alepha.als.set("links", args.links);
487
+ }
481
488
  const { element, layers, redirect, context } = await this.router.render(
482
489
  url.pathname + url.search,
483
490
  {
@@ -492,42 +499,67 @@ class ReactServerProvider {
492
499
  }
493
500
  });
494
501
  }
495
- const appHtml = renderToString(element);
502
+ const html = renderToString(element);
503
+ const $ = load(template);
496
504
  const script = `<script>window.__ssr=${JSON.stringify({
505
+ links: args.links,
497
506
  layers: layers.map((it) => ({
498
507
  ...it,
508
+ error: it.error ? {
509
+ ...it.error,
510
+ name: it.error.name,
511
+ message: it.error.message,
512
+ stack: it.error.stack
513
+ // TODO: Hide stack in production ?
514
+ } : void 0,
499
515
  index: void 0,
500
516
  path: void 0,
501
517
  element: void 0
502
518
  }))
503
519
  })}<\/script>`;
504
- const index = template.indexOf("</body>");
505
- if (index !== -1) {
506
- template = template.slice(0, index) + script + template.slice(index);
520
+ const body = $("body");
521
+ const root = body.find(`#${this.env.REACT_ROOT_ID}`);
522
+ if (root.length) {
523
+ root.html(html);
524
+ } else {
525
+ body.prepend(`<div id="${this.env.REACT_ROOT_ID}">${html}</div>`);
507
526
  }
508
- if (context.helmet) {
509
- template = this.renderHelmetContext(template, context.helmet);
527
+ body.append(script);
528
+ if (context.head) {
529
+ this.renderHeadContext($, context.head);
510
530
  }
511
- template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
512
- return new Response(template, {
531
+ return new Response($.html(), {
513
532
  headers: { "Content-Type": "text/html" }
514
533
  });
515
534
  }
516
- renderHelmetContext(template, helmetContext) {
517
- if (helmetContext.title) {
518
- if (template.includes("<title>")) {
519
- template = template.replace(
520
- /<title>.*<\/title>/,
521
- `<title>${helmetContext.title}</title>`
522
- );
523
- } else {
524
- template = template.replace(
525
- "</head>",
526
- `<title>${helmetContext.title}</title></head>`
527
- );
535
+ renderHeadContext($, headContext) {
536
+ const head = $("head");
537
+ if (head) {
538
+ if (headContext.title) {
539
+ head.find("title").remove();
540
+ head.append(`<title>${headContext.title}</title>`);
541
+ }
542
+ if (headContext.meta) {
543
+ for (const it of headContext.meta) {
544
+ const meta = head.find(`meta[name="${it.name}"]`);
545
+ if (meta.length) {
546
+ meta.attr("content", it.content);
547
+ } else {
548
+ head.append(`<meta name="${it.name}" content="${it.content}" />`);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ if (headContext.htmlAttributes) {
554
+ for (const [key, value] of Object.entries(headContext.htmlAttributes)) {
555
+ $("html").attr(key, value);
556
+ }
557
+ }
558
+ if (headContext.bodyAttributes) {
559
+ for (const [key, value] of Object.entries(headContext.bodyAttributes)) {
560
+ $("body").attr(key, value);
528
561
  }
529
562
  }
530
- return template;
531
563
  }
532
564
  }
533
565
 
@@ -12,7 +12,10 @@ const $auth = (options) => {
12
12
  core.__descriptor(KEY);
13
13
  return {
14
14
  [core.KIND]: KEY,
15
- options
15
+ options,
16
+ jwks: () => {
17
+ return options.oidc?.issuer ?? "";
18
+ }
16
19
  };
17
20
  };
18
21
  $auth[core.KIND] = KEY;
@@ -95,10 +98,7 @@ class Router extends core.EventEmitter {
95
98
  state,
96
99
  router: this,
97
100
  alepha: this.alepha,
98
- args: {
99
- user: context.user,
100
- cookies: context.cookies
101
- }
101
+ args: context
102
102
  }
103
103
  },
104
104
  state.layers[0]?.element
@@ -265,6 +265,7 @@ class Router extends core.EventEmitter {
265
265
  });
266
266
  if (prev === curr) {
267
267
  it.props = previous[i].props;
268
+ it.error = previous[i].error;
268
269
  context = {
269
270
  ...context,
270
271
  ...it.props
@@ -307,7 +308,7 @@ class Router extends core.EventEmitter {
307
308
  for (const key of Object.keys(params2)) {
308
309
  params2[key] = String(params2[key]);
309
310
  }
310
- if (it.route.helmet && renderContext) {
311
+ if (it.route.head && renderContext && !it.error) {
311
312
  this.mergeRenderContext(it.route, renderContext, {
312
313
  ...props,
313
314
  ...context
@@ -318,13 +319,14 @@ class Router extends core.EventEmitter {
318
319
  const path = acc.replace(/\/+/, "/");
319
320
  if (it.error) {
320
321
  const errorHandler = this.getErrorHandler(it.route);
321
- const element = errorHandler ? errorHandler({
322
+ const element = await (errorHandler ? errorHandler({
322
323
  ...it.config,
323
324
  error: it.error,
324
325
  url
325
- }) : this.renderError(it.error);
326
+ }) : this.renderError(it.error));
326
327
  layers.push({
327
328
  props,
329
+ error: it.error,
328
330
  name: it.route.name,
329
331
  part: it.route.path,
330
332
  config: it.config,
@@ -388,15 +390,32 @@ class Router extends core.EventEmitter {
388
390
  * @protected
389
391
  */
390
392
  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}`;
393
+ if (page.head) {
394
+ ctx.head ??= {};
395
+ const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
396
+ if (head.title) {
397
+ ctx.head ??= {};
398
+ if (ctx.head.titleSeparator) {
399
+ ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
397
400
  } else {
398
- ctx.helmet.title = helmet.title;
401
+ ctx.head.title = head.title;
399
402
  }
403
+ ctx.head.titleSeparator = head.titleSeparator;
404
+ }
405
+ if (head.htmlAttributes) {
406
+ ctx.head.htmlAttributes = {
407
+ ...ctx.head.htmlAttributes,
408
+ ...head.htmlAttributes
409
+ };
410
+ }
411
+ if (head.bodyAttributes) {
412
+ ctx.head.bodyAttributes = {
413
+ ...ctx.head.bodyAttributes,
414
+ ...head.bodyAttributes
415
+ };
416
+ }
417
+ if (head.meta) {
418
+ ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
400
419
  }
401
420
  }
402
421
  }
@@ -565,10 +584,14 @@ class PageDescriptorProvider {
565
584
  }
566
585
  }
567
586
 
587
+ const envSchema = core.t.object({
588
+ REACT_ROOT_ID: core.t.string({ default: "root" })
589
+ });
568
590
  class ReactBrowserProvider {
569
591
  log = core.$logger();
570
592
  client = core.$inject(server.HttpClient);
571
593
  router = core.$inject(Router);
594
+ env = core.$inject(envSchema);
572
595
  root;
573
596
  transitioning;
574
597
  state = {
@@ -656,12 +679,49 @@ class ReactBrowserProvider {
656
679
  return await this.render({ url: result.redirect });
657
680
  }
658
681
  this.transitioning = void 0;
659
- return { url };
682
+ return { url, context: result.context };
660
683
  }
661
- renderHelmetContext(ctx) {
684
+ /**
685
+ * Render the helmet context.
686
+ *
687
+ * @param ctx
688
+ * @protected
689
+ */
690
+ renderHeadContext(ctx) {
662
691
  if (ctx.title) {
663
692
  this.document.title = ctx.title;
664
693
  }
694
+ if (ctx.bodyAttributes) {
695
+ for (const [key, value] of Object.entries(ctx.bodyAttributes)) {
696
+ if (value) {
697
+ this.document.body.setAttribute(key, value);
698
+ } else {
699
+ this.document.body.removeAttribute(key);
700
+ }
701
+ }
702
+ }
703
+ if (ctx.htmlAttributes) {
704
+ for (const [key, value] of Object.entries(ctx.htmlAttributes)) {
705
+ if (value) {
706
+ this.document.documentElement.setAttribute(key, value);
707
+ } else {
708
+ this.document.documentElement.removeAttribute(key);
709
+ }
710
+ }
711
+ }
712
+ if (ctx.meta) {
713
+ for (const [key, value] of Object.entries(ctx.meta)) {
714
+ const meta = this.document.querySelector(`meta[name="${key}"]`);
715
+ if (meta) {
716
+ meta.setAttribute("content", value.content);
717
+ } else {
718
+ const newMeta = this.document.createElement("meta");
719
+ newMeta.setAttribute("name", key);
720
+ newMeta.setAttribute("content", value.content);
721
+ this.document.head.appendChild(newMeta);
722
+ }
723
+ }
724
+ }
665
725
  }
666
726
  /**
667
727
  * Get embedded layers from the server.
@@ -682,13 +742,13 @@ class ReactBrowserProvider {
682
742
  * @protected
683
743
  */
684
744
  getRootElement() {
685
- const root = this.document.getElementById("root");
745
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
686
746
  if (root) {
687
747
  return root;
688
748
  }
689
749
  const div = this.document.createElement("div");
690
- div.id = "root";
691
- this.document.body.appendChild(div);
750
+ div.id = this.env.REACT_ROOT_ID;
751
+ this.document.body.prepend(div);
692
752
  return div;
693
753
  }
694
754
  getUserFromCookies() {
@@ -713,7 +773,13 @@ class ReactBrowserProvider {
713
773
  handler: async () => {
714
774
  const cache = this.getHydrationState();
715
775
  const previous = cache?.layers ?? [];
716
- await this.render({ previous });
776
+ if (cache?.links) {
777
+ this.client.links = cache.links;
778
+ }
779
+ const { context } = await this.render({ previous });
780
+ if (context.head) {
781
+ this.renderHeadContext(context.head);
782
+ }
717
783
  const element = this.router.root(this.state, {
718
784
  user: cache?.user ?? this.getUserFromCookies()
719
785
  });
@@ -721,40 +787,30 @@ class ReactBrowserProvider {
721
787
  this.root = client.hydrateRoot(this.getRootElement(), element);
722
788
  this.log.info("Hydrated root element");
723
789
  } else {
724
- this.root = client.createRoot(this.getRootElement());
790
+ this.root ??= client.createRoot(this.getRootElement());
725
791
  this.root.render(element);
726
792
  this.log.info("Created root element");
727
793
  }
728
794
  window.addEventListener("popstate", () => {
729
795
  this.render();
730
796
  });
731
- this.router.on("end", ({ context }) => {
732
- if (context.helmet) {
733
- this.renderHelmetContext(context.helmet);
797
+ this.router.on("end", ({ context: context2 }) => {
798
+ if (context2.head) {
799
+ this.renderHeadContext(context2.head);
734
800
  }
735
801
  });
736
802
  }
737
803
  });
738
- /**
739
- *
740
- * @protected
741
- */
742
- stop = core.$hook({
743
- name: "stop",
744
- handler: async () => {
745
- if (this.root) {
746
- this.root.unmount();
747
- this.log.info("Unmounted root element");
748
- }
749
- }
750
- });
751
804
  }
752
805
 
753
806
  class Auth {
754
807
  alepha = core.$inject(core.Alepha);
755
808
  log = core.$logger();
756
809
  client = core.$inject(server.HttpClient);
757
- api = "/api/_oauth/login";
810
+ slugs = {
811
+ login: "/api/_oauth/login",
812
+ logout: "/api/_oauth/logout"
813
+ };
758
814
  start = core.$hook({
759
815
  name: "start",
760
816
  handler: async () => {
@@ -769,16 +825,16 @@ class Auth {
769
825
  if (this.alepha.isBrowser()) {
770
826
  const browser = this.alepha.get(ReactBrowserProvider);
771
827
  const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
772
- window.location.href = `${this.api}?redirect=${redirect}`;
828
+ window.location.href = `${this.slugs.login}?redirect=${redirect}`;
773
829
  if (browser.transitioning) {
774
830
  throw new RedirectionError(browser.state.pathname);
775
831
  }
776
832
  return;
777
833
  }
778
- throw new RedirectionError(this.api);
834
+ throw new RedirectionError(this.slugs.login);
779
835
  };
780
836
  logout = () => {
781
- window.location.href = `/api/_oauth/logout?redirect=${encodeURIComponent(window.location.origin)}`;
837
+ window.location.href = `${this.slugs.logout}?redirect=${encodeURIComponent(window.location.origin)}`;
782
838
  };
783
839
  }
784
840
 
@@ -908,8 +964,18 @@ const useRouter = () => {
908
964
 
909
965
  const Link = (props) => {
910
966
  React.useContext(RouterContext);
967
+ const to = typeof props.to === "string" ? props.to : props.to.options.path;
968
+ if (!to) {
969
+ return null;
970
+ }
971
+ const can = typeof props.to === "string" ? void 0 : props.to.options.can;
972
+ if (can && !can()) {
973
+ console.log("I cannot go to", to);
974
+ return null;
975
+ }
976
+ const name = typeof props.to === "string" ? void 0 : props.to.options.name;
911
977
  const router = useRouter();
912
- return /* @__PURE__ */ jsxRuntime.jsx("a", { ...router.createAnchorProps(props.to), ...props, children: props.children });
978
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { ...router.createAnchorProps(to), ...props, children: props.children ?? name });
913
979
  };
914
980
 
915
981
  const useInject = (clazz) => {
@@ -917,7 +983,9 @@ const useInject = (clazz) => {
917
983
  if (!ctx) {
918
984
  throw new Error("useRouter must be used within a <RouterProvider>");
919
985
  }
920
- return ctx.alepha.get(clazz);
986
+ return ctx.alepha.get(clazz, {
987
+ skipRegistration: true
988
+ });
921
989
  };
922
990
 
923
991
  const useClient = () => {