@alepha/react 0.9.3 → 0.9.4

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.
Files changed (37) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +315 -320
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +496 -457
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +276 -258
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +274 -256
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +494 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +13 -10
  13. package/src/components/NestedView.tsx +15 -13
  14. package/src/components/NotFound.tsx +1 -1
  15. package/src/descriptors/$page.ts +16 -4
  16. package/src/errors/Redirection.ts +8 -5
  17. package/src/hooks/useActive.ts +25 -34
  18. package/src/hooks/useAlepha.ts +16 -2
  19. package/src/hooks/useClient.ts +7 -4
  20. package/src/hooks/useInject.ts +4 -1
  21. package/src/hooks/useQueryParams.ts +9 -6
  22. package/src/hooks/useRouter.ts +18 -31
  23. package/src/hooks/useRouterEvents.ts +7 -7
  24. package/src/hooks/useRouterState.ts +8 -20
  25. package/src/hooks/useSchema.ts +10 -15
  26. package/src/hooks/useStore.ts +0 -7
  27. package/src/index.browser.ts +11 -11
  28. package/src/index.shared.ts +2 -3
  29. package/src/index.ts +21 -30
  30. package/src/providers/ReactBrowserProvider.ts +149 -65
  31. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  32. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
  33. package/src/providers/ReactServerProvider.ts +69 -74
  34. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
  35. package/src/contexts/RouterContext.ts +0 -14
  36. package/src/providers/BrowserRouterProvider.ts +0 -155
  37. package/src/providers/ReactBrowserRenderer.ts +0 -93
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
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";
1
+ import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
2
+ import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
3
3
  import { AlephaServerCache } from "@alepha/server-cache";
4
4
  import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
5
+ import { $logger } from "@alepha/logger";
5
6
  import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { RouterProvider } from "@alepha/router";
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { ServerStaticProvider } from "@alepha/server-static";
11
11
  import { renderToString } from "react-dom/server";
12
+ import { DateTimeProvider } from "@alepha/datetime";
13
+ import { createRoot, hydrateRoot } from "react-dom/client";
14
+ import { RouterProvider } from "@alepha/router";
12
15
 
13
16
  //#region src/descriptors/$page.ts
14
17
  /**
@@ -34,6 +37,12 @@ var PageDescriptor = class extends Descriptor {
34
37
  async render(options) {
35
38
  throw new Error("render method is not implemented in this environment");
36
39
  }
40
+ match(url) {
41
+ return false;
42
+ }
43
+ pathname(config) {
44
+ return this.options.path || "";
45
+ }
37
46
  };
38
47
  $page[KIND] = PageDescriptor;
39
48
 
@@ -205,28 +214,54 @@ const ErrorViewerProduction = () => {
205
214
  });
206
215
  };
207
216
 
208
- //#endregion
209
- //#region src/contexts/RouterContext.ts
210
- const RouterContext = createContext(void 0);
211
-
212
217
  //#endregion
213
218
  //#region src/contexts/RouterLayerContext.ts
214
219
  const RouterLayerContext = createContext(void 0);
215
220
 
221
+ //#endregion
222
+ //#region src/errors/Redirection.ts
223
+ /**
224
+ * Used for Redirection during the page loading.
225
+ *
226
+ * Depends on the context, it can be thrown or just returned.
227
+ */
228
+ var Redirection = class extends Error {
229
+ redirect;
230
+ constructor(redirect) {
231
+ super("Redirection");
232
+ this.redirect = redirect;
233
+ }
234
+ };
235
+
216
236
  //#endregion
217
237
  //#region src/contexts/AlephaContext.ts
218
238
  const AlephaContext = createContext(void 0);
219
239
 
220
240
  //#endregion
221
241
  //#region src/hooks/useAlepha.ts
242
+ /**
243
+ * Main Alepha hook.
244
+ *
245
+ * It provides access to the Alepha instance within a React component.
246
+ *
247
+ * With Alepha, you can access the core functionalities of the framework:
248
+ *
249
+ * - alepha.state() for state management
250
+ * - alepha.inject() for dependency injection
251
+ * - alepha.emit() for event handling
252
+ * etc...
253
+ */
222
254
  const useAlepha = () => {
223
255
  const alepha = useContext(AlephaContext);
224
- if (!alepha) throw new Error("useAlepha must be used within an AlephaContext.Provider");
256
+ if (!alepha) throw new AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
225
257
  return alepha;
226
258
  };
227
259
 
228
260
  //#endregion
229
261
  //#region src/hooks/useRouterEvents.ts
262
+ /**
263
+ * Subscribe to various router events.
264
+ */
230
265
  const useRouterEvents = (opts = {}, deps = []) => {
231
266
  const alepha = useAlepha();
232
267
  useEffect(() => {
@@ -299,19 +334,21 @@ var ErrorBoundary_default = ErrorBoundary;
299
334
  * ```
300
335
  */
301
336
  const NestedView = (props) => {
302
- const app = useContext(RouterContext);
303
337
  const layer = useContext(RouterLayerContext);
304
338
  const index = layer?.index ?? 0;
305
- const [view, setView] = useState(app?.state.layers[index]?.element);
306
- useRouterEvents({ onEnd: ({ state, context }) => {
307
- if (app) app.context = context;
308
- if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
309
- } }, [app]);
310
- if (!app) throw new Error("NestedView must be used within a RouterContext.");
339
+ const alepha = useAlepha();
340
+ const state = alepha.state("react.router.state");
341
+ if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
342
+ const [view, setView] = useState(state.layers[index]?.element);
343
+ useRouterEvents({ onEnd: ({ state: state$1 }) => {
344
+ if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
345
+ } }, []);
311
346
  const element = view ?? props.children ?? null;
312
347
  return /* @__PURE__ */ jsx(ErrorBoundary_default, {
313
348
  fallback: (error) => {
314
- return app.context.onError?.(error, app.context);
349
+ const result = state.onError(error, state);
350
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
351
+ return result;
315
352
  },
316
353
  children: element
317
354
  });
@@ -338,27 +375,17 @@ function NotFoundPage(props) {
338
375
  fontSize: "1rem",
339
376
  marginBottom: "0.5rem"
340
377
  },
341
- children: "This page does not exist"
378
+ children: "404 - This page does not exist"
342
379
  })
343
380
  });
344
381
  }
345
382
 
346
383
  //#endregion
347
- //#region src/errors/Redirection.ts
348
- var Redirection = class extends Error {
349
- page;
350
- constructor(page) {
351
- super("Redirection");
352
- this.page = page;
353
- }
354
- };
355
-
356
- //#endregion
357
- //#region src/providers/PageDescriptorProvider.ts
358
- const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
359
- var PageDescriptorProvider = class {
384
+ //#region src/providers/ReactPageProvider.ts
385
+ const envSchema$2 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
386
+ var ReactPageProvider = class {
360
387
  log = $logger();
361
- env = $env(envSchema$1);
388
+ env = $env(envSchema$2);
362
389
  alepha = $inject(Alepha);
363
390
  pages = [];
364
391
  getPages() {
@@ -385,22 +412,21 @@ var PageDescriptorProvider = class {
385
412
  return url.replace(/\/\/+/g, "/") || "/";
386
413
  }
387
414
  url(name, options = {}) {
388
- return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
415
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
389
416
  }
390
- root(state, context) {
391
- const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(RouterContext.Provider, { value: {
392
- state,
393
- context
394
- } }, createElement(NestedView_default, {}, state.layers[0]?.element)));
417
+ root(state) {
418
+ const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
395
419
  if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
396
420
  return root;
397
421
  }
398
- async createLayers(route, request) {
399
- const { pathname, search } = request.url;
400
- const layers = [];
422
+ /**
423
+ * Create a new RouterState based on a given route and request.
424
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
425
+ * It also handles errors and redirects.
426
+ */
427
+ async createLayers(route, state, previous = []) {
401
428
  let context = {};
402
429
  const stack = [{ route }];
403
- request.onError = (error) => this.renderError(error);
404
430
  let parent = route.parent;
405
431
  while (parent) {
406
432
  stack.unshift({ route: parent });
@@ -412,19 +438,18 @@ var PageDescriptorProvider = class {
412
438
  const route$1 = it.route;
413
439
  const config = {};
414
440
  try {
415
- config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
441
+ config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
416
442
  } catch (e) {
417
443
  it.error = e;
418
444
  break;
419
445
  }
420
446
  try {
421
- config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
447
+ config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
422
448
  } catch (e) {
423
449
  it.error = e;
424
450
  break;
425
451
  }
426
452
  it.config = { ...config };
427
- const previous = request.previous;
428
453
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
429
454
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
430
455
  const prev = JSON.stringify({
@@ -450,7 +475,7 @@ var PageDescriptorProvider = class {
450
475
  if (!route$1.resolve) continue;
451
476
  try {
452
477
  const props = await route$1.resolve?.({
453
- ...request,
478
+ ...state,
454
479
  ...config,
455
480
  ...context
456
481
  }) ?? {};
@@ -460,11 +485,8 @@ var PageDescriptorProvider = class {
460
485
  ...props
461
486
  };
462
487
  } catch (e) {
463
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
464
- pathname,
465
- search
466
- });
467
- this.log.error(e);
488
+ if (e instanceof Redirection) return { redirect: e.redirect };
489
+ this.log.error("Page resolver has failed", e);
468
490
  it.error = e;
469
491
  break;
470
492
  }
@@ -480,69 +502,58 @@ var PageDescriptorProvider = class {
480
502
  const path = acc.replace(/\/+/, "/");
481
503
  const localErrorHandler = this.getErrorHandler(it.route);
482
504
  if (localErrorHandler) {
483
- const onErrorParent = request.onError;
484
- request.onError = (error, context$1) => {
505
+ const onErrorParent = state.onError;
506
+ state.onError = (error, context$1) => {
485
507
  const result = localErrorHandler(error, context$1);
486
508
  if (result === void 0) return onErrorParent(error, context$1);
487
509
  return result;
488
510
  };
489
511
  }
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
512
+ if (!it.error) try {
513
+ const element = await this.createElement(it.route, {
514
+ ...props,
515
+ ...context
516
+ });
517
+ state.layers.push({
518
+ name: it.route.name,
519
+ props,
520
+ part: it.route.path,
521
+ config: it.config,
522
+ element: this.renderView(i + 1, path, element, it.route),
523
+ index: i + 1,
524
+ path,
525
+ route: it.route,
526
+ cache: it.cache
496
527
  });
497
- if (element$1 === null) element$1 = this.renderError(it.error);
498
- layers.push({
528
+ } catch (e) {
529
+ it.error = e;
530
+ }
531
+ if (it.error) try {
532
+ let element = await state.onError(it.error, state);
533
+ if (element === void 0) throw it.error;
534
+ if (element instanceof Redirection) return { redirect: element.redirect };
535
+ if (element === null) element = this.renderError(it.error);
536
+ state.layers.push({
499
537
  props,
500
538
  error: it.error,
501
539
  name: it.route.name,
502
540
  part: it.route.path,
503
541
  config: it.config,
504
- element: this.renderView(i + 1, path, element$1, it.route),
542
+ element: this.renderView(i + 1, path, element, it.route),
505
543
  index: i + 1,
506
544
  path,
507
545
  route: it.route
508
546
  });
509
547
  break;
510
548
  } catch (e) {
511
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
512
- pathname,
513
- search
514
- });
549
+ if (e instanceof Redirection) return { redirect: e.redirect };
515
550
  throw e;
516
551
  }
517
- const element = await this.createElement(it.route, {
518
- ...props,
519
- ...context
520
- });
521
- layers.push({
522
- name: it.route.name,
523
- props,
524
- part: it.route.path,
525
- config: it.config,
526
- element: this.renderView(i + 1, path, element, it.route),
527
- index: i + 1,
528
- path,
529
- route: it.route,
530
- cache: it.cache
531
- });
532
552
  }
533
- return {
534
- layers,
535
- pathname,
536
- search
537
- };
553
+ return { state };
538
554
  }
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
- };
555
+ createRedirectionLayer(redirect) {
556
+ return { redirect };
546
557
  }
547
558
  getErrorHandler(route) {
548
559
  if (route.errorHandler) return route.errorHandler;
@@ -668,223 +679,9 @@ const isPageRoute = (it) => {
668
679
  return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
669
680
  };
670
681
 
671
- //#endregion
672
- //#region src/providers/BrowserRouterProvider.ts
673
- var BrowserRouterProvider = class extends RouterProvider {
674
- log = $logger();
675
- alepha = $inject(Alepha);
676
- pageDescriptorProvider = $inject(PageDescriptorProvider);
677
- add(entry) {
678
- this.pageDescriptorProvider.add(entry);
679
- }
680
- configure = $hook({
681
- on: "configure",
682
- handler: async () => {
683
- for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
684
- path: page.match,
685
- page
686
- });
687
- }
688
- });
689
- async transition(url, options = {}) {
690
- const { pathname, search } = url;
691
- const state = {
692
- pathname,
693
- search,
694
- layers: []
695
- };
696
- const context = {
697
- url,
698
- query: {},
699
- params: {},
700
- onError: () => null,
701
- ...options.context ?? {}
702
- };
703
- await this.alepha.emit("react:transition:begin", {
704
- state,
705
- context
706
- });
707
- try {
708
- const previous = options.previous;
709
- const { route, params } = this.match(pathname);
710
- const query = {};
711
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
712
- context.query = query;
713
- context.params = params ?? {};
714
- context.previous = previous;
715
- if (isPageRoute(route)) {
716
- const result = await this.pageDescriptorProvider.createLayers(route.page, context);
717
- if (result.redirect) return {
718
- redirect: result.redirect,
719
- state,
720
- context
721
- };
722
- state.layers = result.layers;
723
- }
724
- if (state.layers.length === 0) state.layers.push({
725
- name: "not-found",
726
- element: createElement(NotFoundPage),
727
- index: 0,
728
- path: "/"
729
- });
730
- await this.alepha.emit("react:transition:success", {
731
- state,
732
- context
733
- });
734
- } catch (e) {
735
- this.log.error(e);
736
- state.layers = [{
737
- name: "error",
738
- element: this.pageDescriptorProvider.renderError(e),
739
- index: 0,
740
- path: "/"
741
- }];
742
- await this.alepha.emit("react:transition:error", {
743
- error: e,
744
- state,
745
- context
746
- });
747
- }
748
- if (options.state) {
749
- options.state.layers = state.layers;
750
- options.state.pathname = state.pathname;
751
- options.state.search = state.search;
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
- }
757
- await this.alepha.emit("react:transition:end", {
758
- state: options.state,
759
- context
760
- });
761
- return {
762
- context,
763
- state
764
- };
765
- }
766
- root(state, context) {
767
- return this.pageDescriptorProvider.root(state, context);
768
- }
769
- };
770
-
771
- //#endregion
772
- //#region src/providers/ReactBrowserProvider.ts
773
- var ReactBrowserProvider = class {
774
- log = $logger();
775
- client = $inject(LinkProvider);
776
- alepha = $inject(Alepha);
777
- router = $inject(BrowserRouterProvider);
778
- root;
779
- transitioning;
780
- state = {
781
- layers: [],
782
- pathname: "",
783
- search: ""
784
- };
785
- get document() {
786
- return window.document;
787
- }
788
- get history() {
789
- return window.history;
790
- }
791
- get location() {
792
- return window.location;
793
- }
794
- get url() {
795
- let url = this.location.pathname + this.location.search;
796
- if (import.meta?.env?.BASE_URL) {
797
- url = url.replace(import.meta.env?.BASE_URL, "");
798
- if (!url.startsWith("/")) url = `/${url}`;
799
- }
800
- return url;
801
- }
802
- pushState(url, replace) {
803
- let path = url;
804
- if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
805
- if (replace) this.history.replaceState({}, "", path);
806
- else this.history.pushState({}, "", path);
807
- }
808
- async invalidate(props) {
809
- const previous = [];
810
- if (props) {
811
- const [key] = Object.keys(props);
812
- const value = props[key];
813
- for (const layer of this.state.layers) {
814
- if (layer.props?.[key]) {
815
- previous.push({
816
- ...layer,
817
- props: {
818
- ...layer.props,
819
- [key]: value
820
- }
821
- });
822
- break;
823
- }
824
- previous.push(layer);
825
- }
826
- }
827
- await this.render({ previous });
828
- }
829
- async go(url, options = {}) {
830
- const result = await this.render({ url });
831
- if (result.context.url.pathname + result.context.url.search !== url) {
832
- this.pushState(result.context.url.pathname + result.context.url.search);
833
- return;
834
- }
835
- this.pushState(url, options.replace);
836
- }
837
- async render(options = {}) {
838
- const previous = options.previous ?? this.state.layers;
839
- const url = options.url ?? this.url;
840
- this.transitioning = { to: url };
841
- const result = await this.router.transition(new URL(`http://localhost${url}`), {
842
- previous,
843
- state: this.state
844
- });
845
- if (result.redirect) return await this.render({ url: result.redirect });
846
- this.transitioning = void 0;
847
- return result;
848
- }
849
- /**
850
- * Get embedded layers from the server.
851
- */
852
- getHydrationState() {
853
- try {
854
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
855
- } catch (error) {
856
- console.error(error);
857
- }
858
- }
859
- ready = $hook({
860
- on: "ready",
861
- handler: async () => {
862
- const hydration = this.getHydrationState();
863
- const previous = hydration?.layers ?? [];
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
- });
871
- const { context } = await this.render({ previous });
872
- await this.alepha.emit("react:browser:render", {
873
- state: this.state,
874
- context,
875
- hydration
876
- });
877
- window.addEventListener("popstate", () => {
878
- if (this.state.pathname === this.url) return;
879
- this.render();
880
- });
881
- }
882
- });
883
- };
884
-
885
682
  //#endregion
886
683
  //#region src/providers/ReactServerProvider.ts
887
- const envSchema = t.object({
684
+ const envSchema$1 = t.object({
888
685
  REACT_SERVER_DIST: t.string({ default: "public" }),
889
686
  REACT_SERVER_PREFIX: t.string({ default: "" }),
890
687
  REACT_SSR_ENABLED: t.optional(t.boolean()),
@@ -894,11 +691,11 @@ const envSchema = t.object({
894
691
  var ReactServerProvider = class {
895
692
  log = $logger();
896
693
  alepha = $inject(Alepha);
897
- pageDescriptorProvider = $inject(PageDescriptorProvider);
694
+ pageApi = $inject(ReactPageProvider);
898
695
  serverStaticProvider = $inject(ServerStaticProvider);
899
696
  serverRouterProvider = $inject(ServerRouterProvider);
900
697
  serverTimingProvider = $inject(ServerTimingProvider);
901
- env = $env(envSchema);
698
+ env = $env(envSchema$1);
902
699
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
903
700
  onConfigure = $hook({
904
701
  on: "configure",
@@ -945,7 +742,7 @@ var ReactServerProvider = class {
945
742
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
946
743
  }
947
744
  async registerPages(templateLoader) {
948
- for (const page of this.pageDescriptorProvider.getPages()) {
745
+ for (const page of this.pageApi.getPages()) {
949
746
  if (page.children?.length) continue;
950
747
  this.log.debug(`+ ${page.match} -> ${page.name}`);
951
748
  this.serverRouterProvider.createRoute({
@@ -979,25 +776,30 @@ var ReactServerProvider = class {
979
776
  */
980
777
  createRenderFunction(name, withIndex = false) {
981
778
  return async (options = {}) => {
982
- const page = this.pageDescriptorProvider.page(name);
983
- const url = new URL(this.pageDescriptorProvider.url(name, options));
984
- const context = {
779
+ const page = this.pageApi.page(name);
780
+ const url = new URL(this.pageApi.url(name, options));
781
+ const entry = {
985
782
  url,
986
783
  params: options.params ?? {},
987
784
  query: options.query ?? {},
988
- head: {},
989
- onError: () => null
785
+ onError: () => null,
786
+ layers: []
990
787
  };
991
- await this.alepha.emit("react:server:render:begin", { context });
992
- const state = await this.pageDescriptorProvider.createLayers(page, context);
993
- if (!withIndex && !options.html) return {
994
- context,
995
- html: renderToString(this.pageDescriptorProvider.root(state, context))
996
- };
997
- const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
788
+ const state = entry;
789
+ this.log.trace("Rendering", { url });
790
+ await this.alepha.emit("react:server:render:begin", { state });
791
+ const { redirect } = await this.pageApi.createLayers(page, state);
792
+ if (redirect) throw new AlephaError("Redirection is not supported in this context");
793
+ if (!withIndex && !options.html) {
794
+ this.alepha.state("react.router.state", state);
795
+ return {
796
+ state,
797
+ html: renderToString(this.pageApi.root(state))
798
+ };
799
+ }
800
+ const html = this.renderToHtml(this.template ?? "", state, options.hydration);
998
801
  if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
999
802
  const result = {
1000
- context,
1001
803
  state,
1002
804
  html
1003
805
  };
@@ -1005,89 +807,82 @@ var ReactServerProvider = class {
1005
807
  return result;
1006
808
  };
1007
809
  }
1008
- createHandler(page, templateLoader) {
810
+ createHandler(route, templateLoader) {
1009
811
  return async (serverRequest) => {
1010
812
  const { url, reply, query, params } = serverRequest;
1011
813
  const template = await templateLoader();
1012
814
  if (!template) throw new Error("Template not found");
1013
- this.log.trace("Rendering page", { name: page.name });
1014
- const context = {
815
+ this.log.trace("Rendering page", { name: route.name });
816
+ const entry = {
1015
817
  url,
1016
818
  params,
1017
819
  query,
1018
- head: {},
1019
- onError: () => null
820
+ onError: () => null,
821
+ layers: []
1020
822
  };
1021
- if (this.alepha.has(ServerLinksProvider)) {
1022
- const srv = this.alepha.inject(ServerLinksProvider);
1023
- const schema = apiLinksResponseSchema;
1024
- context.links = this.alepha.parse(schema, await srv.getLinks({
1025
- user: serverRequest.user,
1026
- authorization: serverRequest.headers.authorization
1027
- }));
1028
- this.alepha.context.set("links", context.links);
1029
- }
1030
- let target = page;
823
+ const state = entry;
824
+ if (this.alepha.has(ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
825
+ user: serverRequest.user,
826
+ authorization: serverRequest.headers.authorization
827
+ }));
828
+ let target = route;
1031
829
  while (target) {
1032
- if (page.can && !page.can()) {
830
+ if (route.can && !route.can()) {
1033
831
  reply.status = 403;
1034
832
  reply.headers["content-type"] = "text/plain";
1035
833
  return "Forbidden";
1036
834
  }
1037
835
  target = target.parent;
1038
836
  }
1039
- await this.alepha.emit("react:transition:begin", {
1040
- request: serverRequest,
1041
- context
1042
- });
1043
837
  await this.alepha.emit("react:server:render:begin", {
1044
838
  request: serverRequest,
1045
- context
839
+ state
1046
840
  });
1047
841
  this.serverTimingProvider.beginTiming("createLayers");
1048
- const state = await this.pageDescriptorProvider.createLayers(page, context);
842
+ const { redirect } = await this.pageApi.createLayers(route, state);
1049
843
  this.serverTimingProvider.endTiming("createLayers");
1050
- if (state.redirect) return reply.redirect(state.redirect);
844
+ if (redirect) return reply.redirect(redirect);
1051
845
  reply.headers["content-type"] = "text/html";
1052
846
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
1053
847
  reply.headers.pragma = "no-cache";
1054
848
  reply.headers.expires = "0";
1055
- if (page.cache && serverRequest.user) delete context.links;
1056
- const html = this.renderToHtml(template, state, context);
849
+ const html = this.renderToHtml(template, state);
1057
850
  if (html instanceof Redirection) {
1058
- reply.redirect(typeof html.page === "string" ? html.page : this.pageDescriptorProvider.href(html.page));
851
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
1059
852
  return;
1060
853
  }
1061
854
  const event = {
1062
855
  request: serverRequest,
1063
- context,
1064
856
  state,
1065
857
  html
1066
858
  };
1067
859
  await this.alepha.emit("react:server:render:end", event);
1068
- page.onServerResponse?.(serverRequest);
1069
- this.log.trace("Page rendered", { name: page.name });
860
+ route.onServerResponse?.(serverRequest);
861
+ this.log.trace("Page rendered", { name: route.name });
1070
862
  return event.html;
1071
863
  };
1072
864
  }
1073
- renderToHtml(template, state, context, hydration = true) {
1074
- const element = this.pageDescriptorProvider.root(state, context);
865
+ renderToHtml(template, state, hydration = true) {
866
+ const element = this.pageApi.root(state);
867
+ this.alepha.state("react.router.state", state);
1075
868
  this.serverTimingProvider.beginTiming("renderToString");
1076
869
  let app = "";
1077
870
  try {
1078
871
  app = renderToString(element);
1079
872
  } catch (error) {
1080
- this.log.error("Error during SSR", error);
1081
- const element$1 = context.onError(error, context);
873
+ this.log.error("renderToString has failed, fallback to error handler", error);
874
+ const element$1 = state.onError(error, state);
1082
875
  if (element$1 instanceof Redirection) return element$1;
1083
876
  app = renderToString(element$1);
877
+ this.log.debug("Error handled successfully with fallback");
1084
878
  }
1085
879
  this.serverTimingProvider.endTiming("renderToString");
1086
880
  const response = { html: template };
1087
881
  if (hydration) {
1088
- const { request, context: context$1,...rest } = this.alepha.context.als?.getStore() ?? {};
882
+ const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
1089
883
  const hydrationData = {
1090
- ...rest,
884
+ ...store,
885
+ "react.router.state": void 0,
1091
886
  layers: state.layers.map((it) => ({
1092
887
  ...it,
1093
888
  error: it.error ? {
@@ -1102,7 +897,7 @@ var ReactServerProvider = class {
1102
897
  route: void 0
1103
898
  }))
1104
899
  };
1105
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
900
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1106
901
  this.fillTemplate(response, app, script);
1107
902
  }
1108
903
  return response.html;
@@ -1123,27 +918,260 @@ var ReactServerProvider = class {
1123
918
  };
1124
919
 
1125
920
  //#endregion
1126
- //#region src/hooks/RouterHookApi.ts
1127
- var RouterHookApi = class {
1128
- constructor(pages, context, state, layer, pageApi, browser) {
1129
- this.pages = pages;
1130
- this.context = context;
1131
- this.state = state;
1132
- this.layer = layer;
1133
- this.pageApi = pageApi;
1134
- this.browser = browser;
921
+ //#region src/providers/ReactBrowserRouterProvider.ts
922
+ var ReactBrowserRouterProvider = class extends RouterProvider {
923
+ log = $logger();
924
+ alepha = $inject(Alepha);
925
+ pageApi = $inject(ReactPageProvider);
926
+ add(entry) {
927
+ this.pageApi.add(entry);
928
+ }
929
+ configure = $hook({
930
+ on: "configure",
931
+ handler: async () => {
932
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
933
+ path: page.match,
934
+ page
935
+ });
936
+ }
937
+ });
938
+ async transition(url, previous = []) {
939
+ const { pathname, search } = url;
940
+ const entry = {
941
+ url,
942
+ query: {},
943
+ params: {},
944
+ layers: [],
945
+ onError: () => null
946
+ };
947
+ const state = entry;
948
+ await this.alepha.emit("react:transition:begin", { state });
949
+ try {
950
+ const { route, params } = this.match(pathname);
951
+ const query = {};
952
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
953
+ state.query = query;
954
+ state.params = params ?? {};
955
+ if (isPageRoute(route)) {
956
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
957
+ if (redirect) return redirect;
958
+ }
959
+ if (state.layers.length === 0) state.layers.push({
960
+ name: "not-found",
961
+ element: createElement(NotFoundPage),
962
+ index: 0,
963
+ path: "/"
964
+ });
965
+ await this.alepha.emit("react:transition:success", { state });
966
+ } catch (e) {
967
+ this.log.error("Transition has failed", e);
968
+ state.layers = [{
969
+ name: "error",
970
+ element: this.pageApi.renderError(e),
971
+ index: 0,
972
+ path: "/"
973
+ }];
974
+ await this.alepha.emit("react:transition:error", {
975
+ error: e,
976
+ state
977
+ });
978
+ }
979
+ if (previous) for (let i = 0; i < previous.length; i++) {
980
+ const layer = previous[i];
981
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
982
+ }
983
+ await this.alepha.emit("react:transition:end", { state });
984
+ this.alepha.state("react.router.state", state);
985
+ }
986
+ root(state) {
987
+ return this.pageApi.root(state);
988
+ }
989
+ };
990
+
991
+ //#endregion
992
+ //#region src/providers/ReactBrowserProvider.ts
993
+ const envSchema = t.object({ REACT_ROOT_ID: t.string({ default: "root" }) });
994
+ var ReactBrowserProvider = class {
995
+ env = $env(envSchema);
996
+ log = $logger();
997
+ client = $inject(LinkProvider);
998
+ alepha = $inject(Alepha);
999
+ router = $inject(ReactBrowserRouterProvider);
1000
+ dateTimeProvider = $inject(DateTimeProvider);
1001
+ root;
1002
+ options = { scrollRestoration: "top" };
1003
+ getRootElement() {
1004
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
1005
+ if (root) return root;
1006
+ const div = this.document.createElement("div");
1007
+ div.id = this.env.REACT_ROOT_ID;
1008
+ this.document.body.prepend(div);
1009
+ return div;
1010
+ }
1011
+ transitioning;
1012
+ get state() {
1013
+ return this.alepha.state("react.router.state");
1014
+ }
1015
+ /**
1016
+ * Accessor for Document DOM API.
1017
+ */
1018
+ get document() {
1019
+ return window.document;
1020
+ }
1021
+ /**
1022
+ * Accessor for History DOM API.
1023
+ */
1024
+ get history() {
1025
+ return window.history;
1026
+ }
1027
+ /**
1028
+ * Accessor for Location DOM API.
1029
+ */
1030
+ get location() {
1031
+ return window.location;
1032
+ }
1033
+ get base() {
1034
+ const base = import.meta.env?.BASE_URL;
1035
+ if (!base || base === "/") return "";
1036
+ return base;
1037
+ }
1038
+ get url() {
1039
+ const url = this.location.pathname + this.location.search;
1040
+ if (this.base) return url.replace(this.base, "");
1041
+ return url;
1042
+ }
1043
+ pushState(path, replace) {
1044
+ const url = this.base + path;
1045
+ if (replace) this.history.replaceState({}, "", url);
1046
+ else this.history.pushState({}, "", url);
1047
+ }
1048
+ async invalidate(props) {
1049
+ const previous = [];
1050
+ this.log.trace("Invalidating layers");
1051
+ if (props) {
1052
+ const [key] = Object.keys(props);
1053
+ const value = props[key];
1054
+ for (const layer of this.state.layers) {
1055
+ if (layer.props?.[key]) {
1056
+ previous.push({
1057
+ ...layer,
1058
+ props: {
1059
+ ...layer.props,
1060
+ [key]: value
1061
+ }
1062
+ });
1063
+ break;
1064
+ }
1065
+ previous.push(layer);
1066
+ }
1067
+ }
1068
+ await this.render({ previous });
1069
+ }
1070
+ async go(url, options = {}) {
1071
+ this.log.trace(`Going to ${url}`, {
1072
+ url,
1073
+ options
1074
+ });
1075
+ await this.render({
1076
+ url,
1077
+ previous: options.force ? [] : this.state.layers
1078
+ });
1079
+ if (this.state.url.pathname + this.state.url.search !== url) {
1080
+ this.pushState(this.state.url.pathname + this.state.url.search);
1081
+ return;
1082
+ }
1083
+ this.pushState(url, options.replace);
1084
+ }
1085
+ async render(options = {}) {
1086
+ const previous = options.previous ?? this.state.layers;
1087
+ const url = options.url ?? this.url;
1088
+ const start = this.dateTimeProvider.now();
1089
+ this.transitioning = {
1090
+ to: url,
1091
+ from: this.state?.url.pathname
1092
+ };
1093
+ this.log.debug("Transitioning...", { to: url });
1094
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
1095
+ if (redirect) {
1096
+ this.log.info("Redirecting to", { redirect });
1097
+ return await this.render({ url: redirect });
1098
+ }
1099
+ const ms = this.dateTimeProvider.now().diff(start);
1100
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1101
+ this.transitioning = void 0;
1102
+ }
1103
+ /**
1104
+ * Get embedded layers from the server.
1105
+ */
1106
+ getHydrationState() {
1107
+ try {
1108
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1109
+ } catch (error) {
1110
+ console.error(error);
1111
+ }
1112
+ }
1113
+ onTransitionEnd = $hook({
1114
+ on: "react:transition:end",
1115
+ handler: () => {
1116
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
1117
+ this.log.trace("Restoring scroll position to top");
1118
+ window.scrollTo(0, 0);
1119
+ }
1120
+ }
1121
+ });
1122
+ ready = $hook({
1123
+ on: "ready",
1124
+ handler: async () => {
1125
+ const hydration = this.getHydrationState();
1126
+ const previous = hydration?.layers ?? [];
1127
+ if (hydration) {
1128
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1129
+ }
1130
+ await this.render({ previous });
1131
+ const element = this.router.root(this.state);
1132
+ if (hydration?.layers) {
1133
+ this.root = hydrateRoot(this.getRootElement(), element);
1134
+ this.log.info("Hydrated root element");
1135
+ } else {
1136
+ this.root ??= createRoot(this.getRootElement());
1137
+ this.root.render(element);
1138
+ this.log.info("Created root element");
1139
+ }
1140
+ window.addEventListener("popstate", () => {
1141
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1142
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1143
+ this.render();
1144
+ });
1145
+ }
1146
+ });
1147
+ };
1148
+
1149
+ //#endregion
1150
+ //#region src/services/ReactRouter.ts
1151
+ var ReactRouter = class {
1152
+ alepha = $inject(Alepha);
1153
+ pageApi = $inject(ReactPageProvider);
1154
+ get state() {
1155
+ return this.alepha.state("react.router.state");
1156
+ }
1157
+ get pages() {
1158
+ return this.pageApi.getPages();
1159
+ }
1160
+ get browser() {
1161
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1162
+ return void 0;
1135
1163
  }
1136
1164
  path(name, config = {}) {
1137
1165
  return this.pageApi.pathname(name, {
1138
1166
  params: {
1139
- ...this.context.params,
1167
+ ...this.state.params,
1140
1168
  ...config.params
1141
1169
  },
1142
1170
  query: config.query
1143
1171
  });
1144
1172
  }
1145
1173
  getURL() {
1146
- if (!this.browser) return this.context.url;
1174
+ if (!this.browser) return this.state.url;
1147
1175
  return new URL(this.location.href);
1148
1176
  }
1149
1177
  get location() {
@@ -1154,11 +1182,11 @@ var RouterHookApi = class {
1154
1182
  return this.state;
1155
1183
  }
1156
1184
  get pathname() {
1157
- return this.state.pathname;
1185
+ return this.state.url.pathname;
1158
1186
  }
1159
1187
  get query() {
1160
1188
  const query = {};
1161
- for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
1189
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1162
1190
  return query;
1163
1191
  }
1164
1192
  async back() {
@@ -1170,17 +1198,6 @@ var RouterHookApi = class {
1170
1198
  async invalidate(props) {
1171
1199
  await this.browser?.invalidate(props);
1172
1200
  }
1173
- /**
1174
- * Create a valid href for the given pathname.
1175
- *
1176
- * @param pathname
1177
- * @param layer
1178
- */
1179
- createHref(pathname, layer = this.layer, options = {}) {
1180
- if (typeof pathname === "object") pathname = pathname.options.path ?? "";
1181
- if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
1182
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
1183
- }
1184
1201
  async go(path, options) {
1185
1202
  for (const page of this.pages) if (page.name === path) {
1186
1203
  await this.browser?.go(this.path(path, options), options);
@@ -1195,7 +1212,7 @@ var RouterHookApi = class {
1195
1212
  break;
1196
1213
  }
1197
1214
  return {
1198
- href,
1215
+ href: this.base(href),
1199
1216
  onClick: (ev) => {
1200
1217
  ev.stopPropagation();
1201
1218
  ev.preventDefault();
@@ -1203,6 +1220,11 @@ var RouterHookApi = class {
1203
1220
  }
1204
1221
  };
1205
1222
  }
1223
+ base(path) {
1224
+ const base = import.meta.env?.BASE_URL;
1225
+ if (!base || base === "/") return path;
1226
+ return base + path;
1227
+ }
1206
1228
  /**
1207
1229
  * Set query params.
1208
1230
  *
@@ -1218,17 +1240,35 @@ var RouterHookApi = class {
1218
1240
  }
1219
1241
  };
1220
1242
 
1243
+ //#endregion
1244
+ //#region src/hooks/useInject.ts
1245
+ /**
1246
+ * Hook to inject a service instance.
1247
+ * It's a wrapper of `useAlepha().inject(service)` with a memoization.
1248
+ */
1249
+ const useInject = (service) => {
1250
+ const alepha = useAlepha();
1251
+ return useMemo(() => alepha.inject(service), []);
1252
+ };
1253
+
1221
1254
  //#endregion
1222
1255
  //#region src/hooks/useRouter.ts
1256
+ /**
1257
+ * Use this hook to access the React Router instance.
1258
+ *
1259
+ * You can add a type parameter to specify the type of your application.
1260
+ * This will allow you to use the router in a typesafe way.
1261
+ *
1262
+ * @example
1263
+ * class App {
1264
+ * home = $page();
1265
+ * }
1266
+ *
1267
+ * const router = useRouter<App>();
1268
+ * router.go("home"); // typesafe
1269
+ */
1223
1270
  const useRouter = () => {
1224
- const alepha = useAlepha();
1225
- const ctx = useContext(RouterContext);
1226
- const layer = useContext(RouterLayerContext);
1227
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1228
- const pages = useMemo(() => {
1229
- return alepha.inject(PageDescriptorProvider).getPages();
1230
- }, []);
1231
- return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1271
+ return useInject(ReactRouter);
1232
1272
  };
1233
1273
 
1234
1274
  //#endregion
@@ -1244,46 +1284,6 @@ const Link = (props) => {
1244
1284
  };
1245
1285
  var Link_default = Link;
1246
1286
 
1247
- //#endregion
1248
- //#region src/hooks/useActive.ts
1249
- const useActive = (path) => {
1250
- const router = useRouter();
1251
- const ctx = useContext(RouterContext);
1252
- const layer = useContext(RouterLayerContext);
1253
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1254
- const [current, setCurrent] = useState(ctx.state.pathname);
1255
- const href = useMemo(() => router.createHref(path ?? "", layer), [path, layer]);
1256
- const [isPending, setPending] = useState(false);
1257
- const isActive = current === href || current === `${href}/` || `${current}/` === href;
1258
- useRouterEvents({ onEnd: ({ state }) => {
1259
- path && setCurrent(state.pathname);
1260
- } }, [path]);
1261
- return {
1262
- isPending,
1263
- isActive,
1264
- anchorProps: {
1265
- href,
1266
- onClick: (ev) => {
1267
- ev?.stopPropagation();
1268
- ev?.preventDefault();
1269
- if (isActive) return;
1270
- if (isPending) return;
1271
- setPending(true);
1272
- router.go(href).then(() => {
1273
- setPending(false);
1274
- });
1275
- }
1276
- }
1277
- };
1278
- };
1279
-
1280
- //#endregion
1281
- //#region src/hooks/useInject.ts
1282
- const useInject = (service) => {
1283
- const alepha = useAlepha();
1284
- return useMemo(() => alepha.inject(service), []);
1285
- };
1286
-
1287
1287
  //#endregion
1288
1288
  //#region src/hooks/useStore.ts
1289
1289
  /**
@@ -1301,24 +1301,70 @@ const useStore = (key, defaultValue) => {
1301
1301
  if (ev.key === key) setState(ev.value);
1302
1302
  });
1303
1303
  }, []);
1304
- if (!alepha.isBrowser()) {
1305
- const value = alepha.context.get(key);
1306
- if (value !== null) return [value, (_) => {}];
1307
- }
1308
1304
  return [state, (value) => {
1309
1305
  alepha.state(key, value);
1310
1306
  }];
1311
1307
  };
1312
1308
 
1309
+ //#endregion
1310
+ //#region src/hooks/useRouterState.ts
1311
+ const useRouterState = () => {
1312
+ const [state] = useStore("react.router.state");
1313
+ if (!state) throw new AlephaError("Missing react router state");
1314
+ return state;
1315
+ };
1316
+
1317
+ //#endregion
1318
+ //#region src/hooks/useActive.ts
1319
+ const useActive = (args) => {
1320
+ const router = useRouter();
1321
+ const [isPending, setPending] = useState(false);
1322
+ const state = useRouterState();
1323
+ const current = state.url.pathname;
1324
+ const options = typeof args === "string" ? { href: args } : {
1325
+ ...args,
1326
+ href: args.href
1327
+ };
1328
+ const href = options.href;
1329
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
1330
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
1331
+ return {
1332
+ isPending,
1333
+ isActive,
1334
+ anchorProps: {
1335
+ href: router.base(href),
1336
+ onClick: async (ev) => {
1337
+ ev?.stopPropagation();
1338
+ ev?.preventDefault();
1339
+ if (isActive) return;
1340
+ if (isPending) return;
1341
+ setPending(true);
1342
+ try {
1343
+ await router.go(href);
1344
+ } finally {
1345
+ setPending(false);
1346
+ }
1347
+ }
1348
+ }
1349
+ };
1350
+ };
1351
+
1313
1352
  //#endregion
1314
1353
  //#region src/hooks/useClient.ts
1315
- const useClient = (_scope) => {
1316
- useStore("user");
1317
- return useInject(LinkProvider).client();
1354
+ /**
1355
+ * Hook to get a virtual client for the specified scope.
1356
+ *
1357
+ * It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
1358
+ */
1359
+ const useClient = (scope) => {
1360
+ return useInject(LinkProvider).client(scope);
1318
1361
  };
1319
1362
 
1320
1363
  //#endregion
1321
1364
  //#region src/hooks/useQueryParams.ts
1365
+ /**
1366
+ * Not well tested. Use with caution.
1367
+ */
1322
1368
  const useQueryParams = (schema, options = {}) => {
1323
1369
  const alepha = useAlepha();
1324
1370
  const key = options.key ?? "q";
@@ -1349,29 +1395,17 @@ const decode = (alepha, schema, data) => {
1349
1395
  }
1350
1396
  };
1351
1397
 
1352
- //#endregion
1353
- //#region src/hooks/useRouterState.ts
1354
- const useRouterState = () => {
1355
- const router = useContext(RouterContext);
1356
- const layer = useContext(RouterLayerContext);
1357
- if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1358
- const [state, setState] = useState(router.state);
1359
- useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1360
- return state;
1361
- };
1362
-
1363
1398
  //#endregion
1364
1399
  //#region src/hooks/useSchema.ts
1365
1400
  const useSchema = (action) => {
1366
1401
  const name = action.name;
1367
1402
  const alepha = useAlepha();
1368
1403
  const httpClient = useInject(HttpClient);
1369
- const linkProvider = useInject(LinkProvider);
1370
1404
  const [schema, setSchema] = useState(ssrSchemaLoading(alepha, name));
1371
1405
  useEffect(() => {
1372
1406
  if (!schema.loading) return;
1373
1407
  const opts = { cache: true };
1374
- httpClient.fetch(`${linkProvider.URL_LINKS}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1408
+ httpClient.fetch(`${LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1375
1409
  }, [name]);
1376
1410
  return schema;
1377
1411
  };
@@ -1380,10 +1414,10 @@ const useSchema = (action) => {
1380
1414
  */
1381
1415
  const ssrSchemaLoading = (alepha, name) => {
1382
1416
  if (!alepha.isBrowser()) {
1383
- const links = alepha.context.get("links")?.links ?? [];
1384
- const can = links.find((it) => it.name === name);
1417
+ const linkProvider = alepha.inject(LinkProvider);
1418
+ const can = linkProvider.getServerLinks().find((link) => link.name === name);
1385
1419
  if (can) {
1386
- const schema$1 = alepha.inject(LinkProvider).links?.find((it) => it.name === name)?.schema;
1420
+ const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
1387
1421
  if (schema$1) {
1388
1422
  can.schema = schema$1;
1389
1423
  return schema$1;
@@ -1391,7 +1425,7 @@ const ssrSchemaLoading = (alepha, name) => {
1391
1425
  }
1392
1426
  return { loading: true };
1393
1427
  }
1394
- const schema = alepha.inject(LinkProvider).links?.find((it) => it.name === name)?.schema;
1428
+ const schema = alepha.inject(LinkProvider).links.find((it) => it.name === name)?.schema;
1395
1429
  if (schema) return schema;
1396
1430
  return { loading: true };
1397
1431
  };
@@ -1413,12 +1447,12 @@ const AlephaReact = $module({
1413
1447
  descriptors: [$page],
1414
1448
  services: [
1415
1449
  ReactServerProvider,
1416
- PageDescriptorProvider,
1417
- ReactBrowserProvider
1450
+ ReactPageProvider,
1451
+ ReactRouter
1418
1452
  ],
1419
- register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider)
1453
+ register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
1420
1454
  });
1421
1455
 
1422
1456
  //#endregion
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 };
1457
+ export { $page, AlephaContext, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactPageProvider, ReactRouter, ReactServerProvider, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1424
1458
  //# sourceMappingURL=index.js.map