@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.cjs CHANGED
@@ -25,13 +25,16 @@ const __alepha_core = __toESM(require("@alepha/core"));
25
25
  const __alepha_server = __toESM(require("@alepha/server"));
26
26
  const __alepha_server_cache = __toESM(require("@alepha/server-cache"));
27
27
  const __alepha_server_links = __toESM(require("@alepha/server-links"));
28
+ const __alepha_logger = __toESM(require("@alepha/logger"));
28
29
  const react = __toESM(require("react"));
29
30
  const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
30
- const __alepha_router = __toESM(require("@alepha/router"));
31
31
  const node_fs = __toESM(require("node:fs"));
32
32
  const node_path = __toESM(require("node:path"));
33
33
  const __alepha_server_static = __toESM(require("@alepha/server-static"));
34
34
  const react_dom_server = __toESM(require("react-dom/server"));
35
+ const __alepha_datetime = __toESM(require("@alepha/datetime"));
36
+ const react_dom_client = __toESM(require("react-dom/client"));
37
+ const __alepha_router = __toESM(require("@alepha/router"));
35
38
 
36
39
  //#region src/descriptors/$page.ts
37
40
  /**
@@ -57,6 +60,12 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
57
60
  async render(options) {
58
61
  throw new Error("render method is not implemented in this environment");
59
62
  }
63
+ match(url) {
64
+ return false;
65
+ }
66
+ pathname(config) {
67
+ return this.options.path || "";
68
+ }
60
69
  };
61
70
  $page[__alepha_core.KIND] = PageDescriptor;
62
71
 
@@ -228,28 +237,54 @@ const ErrorViewerProduction = () => {
228
237
  });
229
238
  };
230
239
 
231
- //#endregion
232
- //#region src/contexts/RouterContext.ts
233
- const RouterContext = (0, react.createContext)(void 0);
234
-
235
240
  //#endregion
236
241
  //#region src/contexts/RouterLayerContext.ts
237
242
  const RouterLayerContext = (0, react.createContext)(void 0);
238
243
 
244
+ //#endregion
245
+ //#region src/errors/Redirection.ts
246
+ /**
247
+ * Used for Redirection during the page loading.
248
+ *
249
+ * Depends on the context, it can be thrown or just returned.
250
+ */
251
+ var Redirection = class extends Error {
252
+ redirect;
253
+ constructor(redirect) {
254
+ super("Redirection");
255
+ this.redirect = redirect;
256
+ }
257
+ };
258
+
239
259
  //#endregion
240
260
  //#region src/contexts/AlephaContext.ts
241
261
  const AlephaContext = (0, react.createContext)(void 0);
242
262
 
243
263
  //#endregion
244
264
  //#region src/hooks/useAlepha.ts
265
+ /**
266
+ * Main Alepha hook.
267
+ *
268
+ * It provides access to the Alepha instance within a React component.
269
+ *
270
+ * With Alepha, you can access the core functionalities of the framework:
271
+ *
272
+ * - alepha.state() for state management
273
+ * - alepha.inject() for dependency injection
274
+ * - alepha.emit() for event handling
275
+ * etc...
276
+ */
245
277
  const useAlepha = () => {
246
278
  const alepha = (0, react.useContext)(AlephaContext);
247
- if (!alepha) throw new Error("useAlepha must be used within an AlephaContext.Provider");
279
+ if (!alepha) throw new __alepha_core.AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
248
280
  return alepha;
249
281
  };
250
282
 
251
283
  //#endregion
252
284
  //#region src/hooks/useRouterEvents.ts
285
+ /**
286
+ * Subscribe to various router events.
287
+ */
253
288
  const useRouterEvents = (opts = {}, deps = []) => {
254
289
  const alepha = useAlepha();
255
290
  (0, react.useEffect)(() => {
@@ -322,19 +357,21 @@ var ErrorBoundary_default = ErrorBoundary;
322
357
  * ```
323
358
  */
324
359
  const NestedView = (props) => {
325
- const app = (0, react.useContext)(RouterContext);
326
360
  const layer = (0, react.useContext)(RouterLayerContext);
327
361
  const index = layer?.index ?? 0;
328
- const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
329
- useRouterEvents({ onEnd: ({ state, context }) => {
330
- if (app) app.context = context;
331
- if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
332
- } }, [app]);
333
- if (!app) throw new Error("NestedView must be used within a RouterContext.");
362
+ const alepha = useAlepha();
363
+ const state = alepha.state("react.router.state");
364
+ if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
365
+ const [view, setView] = (0, react.useState)(state.layers[index]?.element);
366
+ useRouterEvents({ onEnd: ({ state: state$1 }) => {
367
+ if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
368
+ } }, []);
334
369
  const element = view ?? props.children ?? null;
335
370
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
336
371
  fallback: (error) => {
337
- return app.context.onError?.(error, app.context);
372
+ const result = state.onError(error, state);
373
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
374
+ return result;
338
375
  },
339
376
  children: element
340
377
  });
@@ -361,27 +398,17 @@ function NotFoundPage(props) {
361
398
  fontSize: "1rem",
362
399
  marginBottom: "0.5rem"
363
400
  },
364
- children: "This page does not exist"
401
+ children: "404 - This page does not exist"
365
402
  })
366
403
  });
367
404
  }
368
405
 
369
406
  //#endregion
370
- //#region src/errors/Redirection.ts
371
- var Redirection = class extends Error {
372
- page;
373
- constructor(page) {
374
- super("Redirection");
375
- this.page = page;
376
- }
377
- };
378
-
379
- //#endregion
380
- //#region src/providers/PageDescriptorProvider.ts
381
- const envSchema$1 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
382
- var PageDescriptorProvider = class {
383
- log = (0, __alepha_core.$logger)();
384
- env = (0, __alepha_core.$env)(envSchema$1);
407
+ //#region src/providers/ReactPageProvider.ts
408
+ const envSchema$2 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
409
+ var ReactPageProvider = class {
410
+ log = (0, __alepha_logger.$logger)();
411
+ env = (0, __alepha_core.$env)(envSchema$2);
385
412
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
386
413
  pages = [];
387
414
  getPages() {
@@ -408,22 +435,21 @@ var PageDescriptorProvider = class {
408
435
  return url.replace(/\/\/+/g, "/") || "/";
409
436
  }
410
437
  url(name, options = {}) {
411
- return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
438
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
412
439
  }
413
- root(state, context) {
414
- const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(RouterContext.Provider, { value: {
415
- state,
416
- context
417
- } }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element)));
440
+ root(state) {
441
+ const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element));
418
442
  if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
419
443
  return root;
420
444
  }
421
- async createLayers(route, request) {
422
- const { pathname, search } = request.url;
423
- const layers = [];
445
+ /**
446
+ * Create a new RouterState based on a given route and request.
447
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
448
+ * It also handles errors and redirects.
449
+ */
450
+ async createLayers(route, state, previous = []) {
424
451
  let context = {};
425
452
  const stack = [{ route }];
426
- request.onError = (error) => this.renderError(error);
427
453
  let parent = route.parent;
428
454
  while (parent) {
429
455
  stack.unshift({ route: parent });
@@ -435,19 +461,18 @@ var PageDescriptorProvider = class {
435
461
  const route$1 = it.route;
436
462
  const config = {};
437
463
  try {
438
- config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
464
+ config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
439
465
  } catch (e) {
440
466
  it.error = e;
441
467
  break;
442
468
  }
443
469
  try {
444
- config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
470
+ config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
445
471
  } catch (e) {
446
472
  it.error = e;
447
473
  break;
448
474
  }
449
475
  it.config = { ...config };
450
- const previous = request.previous;
451
476
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
452
477
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
453
478
  const prev = JSON.stringify({
@@ -473,7 +498,7 @@ var PageDescriptorProvider = class {
473
498
  if (!route$1.resolve) continue;
474
499
  try {
475
500
  const props = await route$1.resolve?.({
476
- ...request,
501
+ ...state,
477
502
  ...config,
478
503
  ...context
479
504
  }) ?? {};
@@ -483,11 +508,8 @@ var PageDescriptorProvider = class {
483
508
  ...props
484
509
  };
485
510
  } catch (e) {
486
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
487
- pathname,
488
- search
489
- });
490
- this.log.error(e);
511
+ if (e instanceof Redirection) return { redirect: e.redirect };
512
+ this.log.error("Page resolver has failed", e);
491
513
  it.error = e;
492
514
  break;
493
515
  }
@@ -503,69 +525,58 @@ var PageDescriptorProvider = class {
503
525
  const path = acc.replace(/\/+/, "/");
504
526
  const localErrorHandler = this.getErrorHandler(it.route);
505
527
  if (localErrorHandler) {
506
- const onErrorParent = request.onError;
507
- request.onError = (error, context$1) => {
528
+ const onErrorParent = state.onError;
529
+ state.onError = (error, context$1) => {
508
530
  const result = localErrorHandler(error, context$1);
509
531
  if (result === void 0) return onErrorParent(error, context$1);
510
532
  return result;
511
533
  };
512
534
  }
513
- if (it.error) try {
514
- let element$1 = await request.onError(it.error, request);
515
- if (element$1 === void 0) throw it.error;
516
- if (element$1 instanceof Redirection) return this.createRedirectionLayer(element$1.page, {
517
- pathname,
518
- search
535
+ if (!it.error) try {
536
+ const element = await this.createElement(it.route, {
537
+ ...props,
538
+ ...context
519
539
  });
520
- if (element$1 === null) element$1 = this.renderError(it.error);
521
- layers.push({
540
+ state.layers.push({
541
+ name: it.route.name,
542
+ props,
543
+ part: it.route.path,
544
+ config: it.config,
545
+ element: this.renderView(i + 1, path, element, it.route),
546
+ index: i + 1,
547
+ path,
548
+ route: it.route,
549
+ cache: it.cache
550
+ });
551
+ } catch (e) {
552
+ it.error = e;
553
+ }
554
+ if (it.error) try {
555
+ let element = await state.onError(it.error, state);
556
+ if (element === void 0) throw it.error;
557
+ if (element instanceof Redirection) return { redirect: element.redirect };
558
+ if (element === null) element = this.renderError(it.error);
559
+ state.layers.push({
522
560
  props,
523
561
  error: it.error,
524
562
  name: it.route.name,
525
563
  part: it.route.path,
526
564
  config: it.config,
527
- element: this.renderView(i + 1, path, element$1, it.route),
565
+ element: this.renderView(i + 1, path, element, it.route),
528
566
  index: i + 1,
529
567
  path,
530
568
  route: it.route
531
569
  });
532
570
  break;
533
571
  } catch (e) {
534
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
535
- pathname,
536
- search
537
- });
572
+ if (e instanceof Redirection) return { redirect: e.redirect };
538
573
  throw e;
539
574
  }
540
- const element = await this.createElement(it.route, {
541
- ...props,
542
- ...context
543
- });
544
- layers.push({
545
- name: it.route.name,
546
- props,
547
- part: it.route.path,
548
- config: it.config,
549
- element: this.renderView(i + 1, path, element, it.route),
550
- index: i + 1,
551
- path,
552
- route: it.route,
553
- cache: it.cache
554
- });
555
575
  }
556
- return {
557
- layers,
558
- pathname,
559
- search
560
- };
576
+ return { state };
561
577
  }
562
- createRedirectionLayer(href, context) {
563
- return {
564
- layers: [],
565
- redirect: typeof href === "string" ? href : this.href(href),
566
- pathname: context.pathname,
567
- search: context.search
568
- };
578
+ createRedirectionLayer(redirect) {
579
+ return { redirect };
569
580
  }
570
581
  getErrorHandler(route) {
571
582
  if (route.errorHandler) return route.errorHandler;
@@ -691,218 +702,9 @@ const isPageRoute = (it) => {
691
702
  return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
692
703
  };
693
704
 
694
- //#endregion
695
- //#region src/providers/BrowserRouterProvider.ts
696
- var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
697
- log = (0, __alepha_core.$logger)();
698
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
699
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
700
- add(entry) {
701
- this.pageDescriptorProvider.add(entry);
702
- }
703
- configure = (0, __alepha_core.$hook)({
704
- on: "configure",
705
- handler: async () => {
706
- for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
707
- path: page.match,
708
- page
709
- });
710
- }
711
- });
712
- async transition(url, options = {}) {
713
- const { pathname, search } = url;
714
- const state = {
715
- pathname,
716
- search,
717
- layers: []
718
- };
719
- const context = {
720
- url,
721
- query: {},
722
- params: {},
723
- onError: () => null,
724
- ...options.context ?? {}
725
- };
726
- await this.alepha.emit("react:transition:begin", {
727
- state,
728
- context
729
- });
730
- try {
731
- const previous = options.previous;
732
- const { route, params } = this.match(pathname);
733
- const query = {};
734
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
735
- context.query = query;
736
- context.params = params ?? {};
737
- context.previous = previous;
738
- if (isPageRoute(route)) {
739
- const result = await this.pageDescriptorProvider.createLayers(route.page, context);
740
- if (result.redirect) return {
741
- redirect: result.redirect,
742
- state,
743
- context
744
- };
745
- state.layers = result.layers;
746
- }
747
- if (state.layers.length === 0) state.layers.push({
748
- name: "not-found",
749
- element: (0, react.createElement)(NotFoundPage),
750
- index: 0,
751
- path: "/"
752
- });
753
- await this.alepha.emit("react:transition:success", {
754
- state,
755
- context
756
- });
757
- } catch (e) {
758
- this.log.error(e);
759
- state.layers = [{
760
- name: "error",
761
- element: this.pageDescriptorProvider.renderError(e),
762
- index: 0,
763
- path: "/"
764
- }];
765
- await this.alepha.emit("react:transition:error", {
766
- error: e,
767
- state,
768
- context
769
- });
770
- }
771
- if (options.state) {
772
- options.state.layers = state.layers;
773
- options.state.pathname = state.pathname;
774
- options.state.search = state.search;
775
- }
776
- if (options.previous) for (let i = 0; i < options.previous.length; i++) {
777
- const layer = options.previous[i];
778
- if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
779
- }
780
- await this.alepha.emit("react:transition:end", {
781
- state: options.state,
782
- context
783
- });
784
- return {
785
- context,
786
- state
787
- };
788
- }
789
- root(state, context) {
790
- return this.pageDescriptorProvider.root(state, context);
791
- }
792
- };
793
-
794
- //#endregion
795
- //#region src/providers/ReactBrowserProvider.ts
796
- var ReactBrowserProvider = class {
797
- log = (0, __alepha_core.$logger)();
798
- client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
799
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
800
- router = (0, __alepha_core.$inject)(BrowserRouterProvider);
801
- root;
802
- transitioning;
803
- state = {
804
- layers: [],
805
- pathname: "",
806
- search: ""
807
- };
808
- get document() {
809
- return window.document;
810
- }
811
- get history() {
812
- return window.history;
813
- }
814
- get location() {
815
- return window.location;
816
- }
817
- get url() {
818
- let url = this.location.pathname + this.location.search;
819
- return url;
820
- }
821
- pushState(url, replace) {
822
- let path = url;
823
- if (replace) this.history.replaceState({}, "", path);
824
- else this.history.pushState({}, "", path);
825
- }
826
- async invalidate(props) {
827
- const previous = [];
828
- if (props) {
829
- const [key] = Object.keys(props);
830
- const value = props[key];
831
- for (const layer of this.state.layers) {
832
- if (layer.props?.[key]) {
833
- previous.push({
834
- ...layer,
835
- props: {
836
- ...layer.props,
837
- [key]: value
838
- }
839
- });
840
- break;
841
- }
842
- previous.push(layer);
843
- }
844
- }
845
- await this.render({ previous });
846
- }
847
- async go(url, options = {}) {
848
- const result = await this.render({ url });
849
- if (result.context.url.pathname + result.context.url.search !== url) {
850
- this.pushState(result.context.url.pathname + result.context.url.search);
851
- return;
852
- }
853
- this.pushState(url, options.replace);
854
- }
855
- async render(options = {}) {
856
- const previous = options.previous ?? this.state.layers;
857
- const url = options.url ?? this.url;
858
- this.transitioning = { to: url };
859
- const result = await this.router.transition(new URL(`http://localhost${url}`), {
860
- previous,
861
- state: this.state
862
- });
863
- if (result.redirect) return await this.render({ url: result.redirect });
864
- this.transitioning = void 0;
865
- return result;
866
- }
867
- /**
868
- * Get embedded layers from the server.
869
- */
870
- getHydrationState() {
871
- try {
872
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
873
- } catch (error) {
874
- console.error(error);
875
- }
876
- }
877
- ready = (0, __alepha_core.$hook)({
878
- on: "ready",
879
- handler: async () => {
880
- const hydration = this.getHydrationState();
881
- const previous = hydration?.layers ?? [];
882
- if (hydration) {
883
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
884
- }
885
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
886
- ...link,
887
- prefix: hydration.links.prefix
888
- });
889
- const { context } = await this.render({ previous });
890
- await this.alepha.emit("react:browser:render", {
891
- state: this.state,
892
- context,
893
- hydration
894
- });
895
- window.addEventListener("popstate", () => {
896
- if (this.state.pathname === this.url) return;
897
- this.render();
898
- });
899
- }
900
- });
901
- };
902
-
903
705
  //#endregion
904
706
  //#region src/providers/ReactServerProvider.ts
905
- const envSchema = __alepha_core.t.object({
707
+ const envSchema$1 = __alepha_core.t.object({
906
708
  REACT_SERVER_DIST: __alepha_core.t.string({ default: "public" }),
907
709
  REACT_SERVER_PREFIX: __alepha_core.t.string({ default: "" }),
908
710
  REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
@@ -910,13 +712,13 @@ const envSchema = __alepha_core.t.object({
910
712
  REACT_SERVER_TEMPLATE: __alepha_core.t.optional(__alepha_core.t.string({ size: "rich" }))
911
713
  });
912
714
  var ReactServerProvider = class {
913
- log = (0, __alepha_core.$logger)();
715
+ log = (0, __alepha_logger.$logger)();
914
716
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
915
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
717
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
916
718
  serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
917
719
  serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
918
720
  serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
919
- env = (0, __alepha_core.$env)(envSchema);
721
+ env = (0, __alepha_core.$env)(envSchema$1);
920
722
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
921
723
  onConfigure = (0, __alepha_core.$hook)({
922
724
  on: "configure",
@@ -963,7 +765,7 @@ var ReactServerProvider = class {
963
765
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
964
766
  }
965
767
  async registerPages(templateLoader) {
966
- for (const page of this.pageDescriptorProvider.getPages()) {
768
+ for (const page of this.pageApi.getPages()) {
967
769
  if (page.children?.length) continue;
968
770
  this.log.debug(`+ ${page.match} -> ${page.name}`);
969
771
  this.serverRouterProvider.createRoute({
@@ -997,25 +799,30 @@ var ReactServerProvider = class {
997
799
  */
998
800
  createRenderFunction(name, withIndex = false) {
999
801
  return async (options = {}) => {
1000
- const page = this.pageDescriptorProvider.page(name);
1001
- const url = new URL(this.pageDescriptorProvider.url(name, options));
1002
- const context = {
802
+ const page = this.pageApi.page(name);
803
+ const url = new URL(this.pageApi.url(name, options));
804
+ const entry = {
1003
805
  url,
1004
806
  params: options.params ?? {},
1005
807
  query: options.query ?? {},
1006
- head: {},
1007
- onError: () => null
808
+ onError: () => null,
809
+ layers: []
1008
810
  };
1009
- await this.alepha.emit("react:server:render:begin", { context });
1010
- const state = await this.pageDescriptorProvider.createLayers(page, context);
1011
- if (!withIndex && !options.html) return {
1012
- context,
1013
- html: (0, react_dom_server.renderToString)(this.pageDescriptorProvider.root(state, context))
1014
- };
1015
- const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
811
+ const state = entry;
812
+ this.log.trace("Rendering", { url });
813
+ await this.alepha.emit("react:server:render:begin", { state });
814
+ const { redirect } = await this.pageApi.createLayers(page, state);
815
+ if (redirect) throw new __alepha_core.AlephaError("Redirection is not supported in this context");
816
+ if (!withIndex && !options.html) {
817
+ this.alepha.state("react.router.state", state);
818
+ return {
819
+ state,
820
+ html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
821
+ };
822
+ }
823
+ const html = this.renderToHtml(this.template ?? "", state, options.hydration);
1016
824
  if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
1017
825
  const result = {
1018
- context,
1019
826
  state,
1020
827
  html
1021
828
  };
@@ -1023,89 +830,82 @@ var ReactServerProvider = class {
1023
830
  return result;
1024
831
  };
1025
832
  }
1026
- createHandler(page, templateLoader) {
833
+ createHandler(route, templateLoader) {
1027
834
  return async (serverRequest) => {
1028
835
  const { url, reply, query, params } = serverRequest;
1029
836
  const template = await templateLoader();
1030
837
  if (!template) throw new Error("Template not found");
1031
- this.log.trace("Rendering page", { name: page.name });
1032
- const context = {
838
+ this.log.trace("Rendering page", { name: route.name });
839
+ const entry = {
1033
840
  url,
1034
841
  params,
1035
842
  query,
1036
- head: {},
1037
- onError: () => null
843
+ onError: () => null,
844
+ layers: []
1038
845
  };
1039
- if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) {
1040
- const srv = this.alepha.inject(__alepha_server_links.ServerLinksProvider);
1041
- const schema = __alepha_server.apiLinksResponseSchema;
1042
- context.links = this.alepha.parse(schema, await srv.getLinks({
1043
- user: serverRequest.user,
1044
- authorization: serverRequest.headers.authorization
1045
- }));
1046
- this.alepha.context.set("links", context.links);
1047
- }
1048
- let target = page;
846
+ const state = entry;
847
+ if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
848
+ user: serverRequest.user,
849
+ authorization: serverRequest.headers.authorization
850
+ }));
851
+ let target = route;
1049
852
  while (target) {
1050
- if (page.can && !page.can()) {
853
+ if (route.can && !route.can()) {
1051
854
  reply.status = 403;
1052
855
  reply.headers["content-type"] = "text/plain";
1053
856
  return "Forbidden";
1054
857
  }
1055
858
  target = target.parent;
1056
859
  }
1057
- await this.alepha.emit("react:transition:begin", {
1058
- request: serverRequest,
1059
- context
1060
- });
1061
860
  await this.alepha.emit("react:server:render:begin", {
1062
861
  request: serverRequest,
1063
- context
862
+ state
1064
863
  });
1065
864
  this.serverTimingProvider.beginTiming("createLayers");
1066
- const state = await this.pageDescriptorProvider.createLayers(page, context);
865
+ const { redirect } = await this.pageApi.createLayers(route, state);
1067
866
  this.serverTimingProvider.endTiming("createLayers");
1068
- if (state.redirect) return reply.redirect(state.redirect);
867
+ if (redirect) return reply.redirect(redirect);
1069
868
  reply.headers["content-type"] = "text/html";
1070
869
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
1071
870
  reply.headers.pragma = "no-cache";
1072
871
  reply.headers.expires = "0";
1073
- if (page.cache && serverRequest.user) delete context.links;
1074
- const html = this.renderToHtml(template, state, context);
872
+ const html = this.renderToHtml(template, state);
1075
873
  if (html instanceof Redirection) {
1076
- reply.redirect(typeof html.page === "string" ? html.page : this.pageDescriptorProvider.href(html.page));
874
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
1077
875
  return;
1078
876
  }
1079
877
  const event = {
1080
878
  request: serverRequest,
1081
- context,
1082
879
  state,
1083
880
  html
1084
881
  };
1085
882
  await this.alepha.emit("react:server:render:end", event);
1086
- page.onServerResponse?.(serverRequest);
1087
- this.log.trace("Page rendered", { name: page.name });
883
+ route.onServerResponse?.(serverRequest);
884
+ this.log.trace("Page rendered", { name: route.name });
1088
885
  return event.html;
1089
886
  };
1090
887
  }
1091
- renderToHtml(template, state, context, hydration = true) {
1092
- const element = this.pageDescriptorProvider.root(state, context);
888
+ renderToHtml(template, state, hydration = true) {
889
+ const element = this.pageApi.root(state);
890
+ this.alepha.state("react.router.state", state);
1093
891
  this.serverTimingProvider.beginTiming("renderToString");
1094
892
  let app = "";
1095
893
  try {
1096
894
  app = (0, react_dom_server.renderToString)(element);
1097
895
  } catch (error) {
1098
- this.log.error("Error during SSR", error);
1099
- const element$1 = context.onError(error, context);
896
+ this.log.error("renderToString has failed, fallback to error handler", error);
897
+ const element$1 = state.onError(error, state);
1100
898
  if (element$1 instanceof Redirection) return element$1;
1101
899
  app = (0, react_dom_server.renderToString)(element$1);
900
+ this.log.debug("Error handled successfully with fallback");
1102
901
  }
1103
902
  this.serverTimingProvider.endTiming("renderToString");
1104
903
  const response = { html: template };
1105
904
  if (hydration) {
1106
- const { request, context: context$1,...rest } = this.alepha.context.als?.getStore() ?? {};
905
+ const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
1107
906
  const hydrationData = {
1108
- ...rest,
907
+ ...store,
908
+ "react.router.state": void 0,
1109
909
  layers: state.layers.map((it) => ({
1110
910
  ...it,
1111
911
  error: it.error ? {
@@ -1120,7 +920,7 @@ var ReactServerProvider = class {
1120
920
  route: void 0
1121
921
  }))
1122
922
  };
1123
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
923
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1124
924
  this.fillTemplate(response, app, script);
1125
925
  }
1126
926
  return response.html;
@@ -1141,27 +941,260 @@ var ReactServerProvider = class {
1141
941
  };
1142
942
 
1143
943
  //#endregion
1144
- //#region src/hooks/RouterHookApi.ts
1145
- var RouterHookApi = class {
1146
- constructor(pages, context, state, layer, pageApi, browser) {
1147
- this.pages = pages;
1148
- this.context = context;
1149
- this.state = state;
1150
- this.layer = layer;
1151
- this.pageApi = pageApi;
1152
- this.browser = browser;
944
+ //#region src/providers/ReactBrowserRouterProvider.ts
945
+ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
946
+ log = (0, __alepha_logger.$logger)();
947
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
948
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
949
+ add(entry) {
950
+ this.pageApi.add(entry);
951
+ }
952
+ configure = (0, __alepha_core.$hook)({
953
+ on: "configure",
954
+ handler: async () => {
955
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
956
+ path: page.match,
957
+ page
958
+ });
959
+ }
960
+ });
961
+ async transition(url, previous = []) {
962
+ const { pathname, search } = url;
963
+ const entry = {
964
+ url,
965
+ query: {},
966
+ params: {},
967
+ layers: [],
968
+ onError: () => null
969
+ };
970
+ const state = entry;
971
+ await this.alepha.emit("react:transition:begin", { state });
972
+ try {
973
+ const { route, params } = this.match(pathname);
974
+ const query = {};
975
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
976
+ state.query = query;
977
+ state.params = params ?? {};
978
+ if (isPageRoute(route)) {
979
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
980
+ if (redirect) return redirect;
981
+ }
982
+ if (state.layers.length === 0) state.layers.push({
983
+ name: "not-found",
984
+ element: (0, react.createElement)(NotFoundPage),
985
+ index: 0,
986
+ path: "/"
987
+ });
988
+ await this.alepha.emit("react:transition:success", { state });
989
+ } catch (e) {
990
+ this.log.error("Transition has failed", e);
991
+ state.layers = [{
992
+ name: "error",
993
+ element: this.pageApi.renderError(e),
994
+ index: 0,
995
+ path: "/"
996
+ }];
997
+ await this.alepha.emit("react:transition:error", {
998
+ error: e,
999
+ state
1000
+ });
1001
+ }
1002
+ if (previous) for (let i = 0; i < previous.length; i++) {
1003
+ const layer = previous[i];
1004
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1005
+ }
1006
+ await this.alepha.emit("react:transition:end", { state });
1007
+ this.alepha.state("react.router.state", state);
1008
+ }
1009
+ root(state) {
1010
+ return this.pageApi.root(state);
1011
+ }
1012
+ };
1013
+
1014
+ //#endregion
1015
+ //#region src/providers/ReactBrowserProvider.ts
1016
+ const envSchema = __alepha_core.t.object({ REACT_ROOT_ID: __alepha_core.t.string({ default: "root" }) });
1017
+ var ReactBrowserProvider = class {
1018
+ env = (0, __alepha_core.$env)(envSchema);
1019
+ log = (0, __alepha_logger.$logger)();
1020
+ client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
1021
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1022
+ router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
1023
+ dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
1024
+ root;
1025
+ options = { scrollRestoration: "top" };
1026
+ getRootElement() {
1027
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
1028
+ if (root) return root;
1029
+ const div = this.document.createElement("div");
1030
+ div.id = this.env.REACT_ROOT_ID;
1031
+ this.document.body.prepend(div);
1032
+ return div;
1033
+ }
1034
+ transitioning;
1035
+ get state() {
1036
+ return this.alepha.state("react.router.state");
1037
+ }
1038
+ /**
1039
+ * Accessor for Document DOM API.
1040
+ */
1041
+ get document() {
1042
+ return window.document;
1043
+ }
1044
+ /**
1045
+ * Accessor for History DOM API.
1046
+ */
1047
+ get history() {
1048
+ return window.history;
1049
+ }
1050
+ /**
1051
+ * Accessor for Location DOM API.
1052
+ */
1053
+ get location() {
1054
+ return window.location;
1055
+ }
1056
+ get base() {
1057
+ const base = {}.env?.BASE_URL;
1058
+ if (!base || base === "/") return "";
1059
+ return base;
1060
+ }
1061
+ get url() {
1062
+ const url = this.location.pathname + this.location.search;
1063
+ if (this.base) return url.replace(this.base, "");
1064
+ return url;
1065
+ }
1066
+ pushState(path, replace) {
1067
+ const url = this.base + path;
1068
+ if (replace) this.history.replaceState({}, "", url);
1069
+ else this.history.pushState({}, "", url);
1070
+ }
1071
+ async invalidate(props) {
1072
+ const previous = [];
1073
+ this.log.trace("Invalidating layers");
1074
+ if (props) {
1075
+ const [key] = Object.keys(props);
1076
+ const value = props[key];
1077
+ for (const layer of this.state.layers) {
1078
+ if (layer.props?.[key]) {
1079
+ previous.push({
1080
+ ...layer,
1081
+ props: {
1082
+ ...layer.props,
1083
+ [key]: value
1084
+ }
1085
+ });
1086
+ break;
1087
+ }
1088
+ previous.push(layer);
1089
+ }
1090
+ }
1091
+ await this.render({ previous });
1092
+ }
1093
+ async go(url, options = {}) {
1094
+ this.log.trace(`Going to ${url}`, {
1095
+ url,
1096
+ options
1097
+ });
1098
+ await this.render({
1099
+ url,
1100
+ previous: options.force ? [] : this.state.layers
1101
+ });
1102
+ if (this.state.url.pathname + this.state.url.search !== url) {
1103
+ this.pushState(this.state.url.pathname + this.state.url.search);
1104
+ return;
1105
+ }
1106
+ this.pushState(url, options.replace);
1107
+ }
1108
+ async render(options = {}) {
1109
+ const previous = options.previous ?? this.state.layers;
1110
+ const url = options.url ?? this.url;
1111
+ const start = this.dateTimeProvider.now();
1112
+ this.transitioning = {
1113
+ to: url,
1114
+ from: this.state?.url.pathname
1115
+ };
1116
+ this.log.debug("Transitioning...", { to: url });
1117
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
1118
+ if (redirect) {
1119
+ this.log.info("Redirecting to", { redirect });
1120
+ return await this.render({ url: redirect });
1121
+ }
1122
+ const ms = this.dateTimeProvider.now().diff(start);
1123
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1124
+ this.transitioning = void 0;
1125
+ }
1126
+ /**
1127
+ * Get embedded layers from the server.
1128
+ */
1129
+ getHydrationState() {
1130
+ try {
1131
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1132
+ } catch (error) {
1133
+ console.error(error);
1134
+ }
1135
+ }
1136
+ onTransitionEnd = (0, __alepha_core.$hook)({
1137
+ on: "react:transition:end",
1138
+ handler: () => {
1139
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
1140
+ this.log.trace("Restoring scroll position to top");
1141
+ window.scrollTo(0, 0);
1142
+ }
1143
+ }
1144
+ });
1145
+ ready = (0, __alepha_core.$hook)({
1146
+ on: "ready",
1147
+ handler: async () => {
1148
+ const hydration = this.getHydrationState();
1149
+ const previous = hydration?.layers ?? [];
1150
+ if (hydration) {
1151
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1152
+ }
1153
+ await this.render({ previous });
1154
+ const element = this.router.root(this.state);
1155
+ if (hydration?.layers) {
1156
+ this.root = (0, react_dom_client.hydrateRoot)(this.getRootElement(), element);
1157
+ this.log.info("Hydrated root element");
1158
+ } else {
1159
+ this.root ??= (0, react_dom_client.createRoot)(this.getRootElement());
1160
+ this.root.render(element);
1161
+ this.log.info("Created root element");
1162
+ }
1163
+ window.addEventListener("popstate", () => {
1164
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1165
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1166
+ this.render();
1167
+ });
1168
+ }
1169
+ });
1170
+ };
1171
+
1172
+ //#endregion
1173
+ //#region src/services/ReactRouter.ts
1174
+ var ReactRouter = class {
1175
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1176
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1177
+ get state() {
1178
+ return this.alepha.state("react.router.state");
1179
+ }
1180
+ get pages() {
1181
+ return this.pageApi.getPages();
1182
+ }
1183
+ get browser() {
1184
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1185
+ return void 0;
1153
1186
  }
1154
1187
  path(name, config = {}) {
1155
1188
  return this.pageApi.pathname(name, {
1156
1189
  params: {
1157
- ...this.context.params,
1190
+ ...this.state.params,
1158
1191
  ...config.params
1159
1192
  },
1160
1193
  query: config.query
1161
1194
  });
1162
1195
  }
1163
1196
  getURL() {
1164
- if (!this.browser) return this.context.url;
1197
+ if (!this.browser) return this.state.url;
1165
1198
  return new URL(this.location.href);
1166
1199
  }
1167
1200
  get location() {
@@ -1172,11 +1205,11 @@ var RouterHookApi = class {
1172
1205
  return this.state;
1173
1206
  }
1174
1207
  get pathname() {
1175
- return this.state.pathname;
1208
+ return this.state.url.pathname;
1176
1209
  }
1177
1210
  get query() {
1178
1211
  const query = {};
1179
- for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
1212
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1180
1213
  return query;
1181
1214
  }
1182
1215
  async back() {
@@ -1188,17 +1221,6 @@ var RouterHookApi = class {
1188
1221
  async invalidate(props) {
1189
1222
  await this.browser?.invalidate(props);
1190
1223
  }
1191
- /**
1192
- * Create a valid href for the given pathname.
1193
- *
1194
- * @param pathname
1195
- * @param layer
1196
- */
1197
- createHref(pathname, layer = this.layer, options = {}) {
1198
- if (typeof pathname === "object") pathname = pathname.options.path ?? "";
1199
- if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
1200
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
1201
- }
1202
1224
  async go(path, options) {
1203
1225
  for (const page of this.pages) if (page.name === path) {
1204
1226
  await this.browser?.go(this.path(path, options), options);
@@ -1213,7 +1235,7 @@ var RouterHookApi = class {
1213
1235
  break;
1214
1236
  }
1215
1237
  return {
1216
- href,
1238
+ href: this.base(href),
1217
1239
  onClick: (ev) => {
1218
1240
  ev.stopPropagation();
1219
1241
  ev.preventDefault();
@@ -1221,6 +1243,11 @@ var RouterHookApi = class {
1221
1243
  }
1222
1244
  };
1223
1245
  }
1246
+ base(path) {
1247
+ const base = {}.env?.BASE_URL;
1248
+ if (!base || base === "/") return path;
1249
+ return base + path;
1250
+ }
1224
1251
  /**
1225
1252
  * Set query params.
1226
1253
  *
@@ -1236,17 +1263,35 @@ var RouterHookApi = class {
1236
1263
  }
1237
1264
  };
1238
1265
 
1266
+ //#endregion
1267
+ //#region src/hooks/useInject.ts
1268
+ /**
1269
+ * Hook to inject a service instance.
1270
+ * It's a wrapper of `useAlepha().inject(service)` with a memoization.
1271
+ */
1272
+ const useInject = (service) => {
1273
+ const alepha = useAlepha();
1274
+ return (0, react.useMemo)(() => alepha.inject(service), []);
1275
+ };
1276
+
1239
1277
  //#endregion
1240
1278
  //#region src/hooks/useRouter.ts
1279
+ /**
1280
+ * Use this hook to access the React Router instance.
1281
+ *
1282
+ * You can add a type parameter to specify the type of your application.
1283
+ * This will allow you to use the router in a typesafe way.
1284
+ *
1285
+ * @example
1286
+ * class App {
1287
+ * home = $page();
1288
+ * }
1289
+ *
1290
+ * const router = useRouter<App>();
1291
+ * router.go("home"); // typesafe
1292
+ */
1241
1293
  const useRouter = () => {
1242
- const alepha = useAlepha();
1243
- const ctx = (0, react.useContext)(RouterContext);
1244
- const layer = (0, react.useContext)(RouterLayerContext);
1245
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1246
- const pages = (0, react.useMemo)(() => {
1247
- return alepha.inject(PageDescriptorProvider).getPages();
1248
- }, []);
1249
- return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1294
+ return useInject(ReactRouter);
1250
1295
  };
1251
1296
 
1252
1297
  //#endregion
@@ -1262,46 +1307,6 @@ const Link = (props) => {
1262
1307
  };
1263
1308
  var Link_default = Link;
1264
1309
 
1265
- //#endregion
1266
- //#region src/hooks/useActive.ts
1267
- const useActive = (path) => {
1268
- const router = useRouter();
1269
- const ctx = (0, react.useContext)(RouterContext);
1270
- const layer = (0, react.useContext)(RouterLayerContext);
1271
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1272
- const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
1273
- const href = (0, react.useMemo)(() => router.createHref(path ?? "", layer), [path, layer]);
1274
- const [isPending, setPending] = (0, react.useState)(false);
1275
- const isActive = current === href || current === `${href}/` || `${current}/` === href;
1276
- useRouterEvents({ onEnd: ({ state }) => {
1277
- path && setCurrent(state.pathname);
1278
- } }, [path]);
1279
- return {
1280
- isPending,
1281
- isActive,
1282
- anchorProps: {
1283
- href,
1284
- onClick: (ev) => {
1285
- ev?.stopPropagation();
1286
- ev?.preventDefault();
1287
- if (isActive) return;
1288
- if (isPending) return;
1289
- setPending(true);
1290
- router.go(href).then(() => {
1291
- setPending(false);
1292
- });
1293
- }
1294
- }
1295
- };
1296
- };
1297
-
1298
- //#endregion
1299
- //#region src/hooks/useInject.ts
1300
- const useInject = (service) => {
1301
- const alepha = useAlepha();
1302
- return (0, react.useMemo)(() => alepha.inject(service), []);
1303
- };
1304
-
1305
1310
  //#endregion
1306
1311
  //#region src/hooks/useStore.ts
1307
1312
  /**
@@ -1319,24 +1324,70 @@ const useStore = (key, defaultValue) => {
1319
1324
  if (ev.key === key) setState(ev.value);
1320
1325
  });
1321
1326
  }, []);
1322
- if (!alepha.isBrowser()) {
1323
- const value = alepha.context.get(key);
1324
- if (value !== null) return [value, (_) => {}];
1325
- }
1326
1327
  return [state, (value) => {
1327
1328
  alepha.state(key, value);
1328
1329
  }];
1329
1330
  };
1330
1331
 
1332
+ //#endregion
1333
+ //#region src/hooks/useRouterState.ts
1334
+ const useRouterState = () => {
1335
+ const [state] = useStore("react.router.state");
1336
+ if (!state) throw new __alepha_core.AlephaError("Missing react router state");
1337
+ return state;
1338
+ };
1339
+
1340
+ //#endregion
1341
+ //#region src/hooks/useActive.ts
1342
+ const useActive = (args) => {
1343
+ const router = useRouter();
1344
+ const [isPending, setPending] = (0, react.useState)(false);
1345
+ const state = useRouterState();
1346
+ const current = state.url.pathname;
1347
+ const options = typeof args === "string" ? { href: args } : {
1348
+ ...args,
1349
+ href: args.href
1350
+ };
1351
+ const href = options.href;
1352
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
1353
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
1354
+ return {
1355
+ isPending,
1356
+ isActive,
1357
+ anchorProps: {
1358
+ href: router.base(href),
1359
+ onClick: async (ev) => {
1360
+ ev?.stopPropagation();
1361
+ ev?.preventDefault();
1362
+ if (isActive) return;
1363
+ if (isPending) return;
1364
+ setPending(true);
1365
+ try {
1366
+ await router.go(href);
1367
+ } finally {
1368
+ setPending(false);
1369
+ }
1370
+ }
1371
+ }
1372
+ };
1373
+ };
1374
+
1331
1375
  //#endregion
1332
1376
  //#region src/hooks/useClient.ts
1333
- const useClient = (_scope) => {
1334
- useStore("user");
1335
- return useInject(__alepha_server_links.LinkProvider).client();
1377
+ /**
1378
+ * Hook to get a virtual client for the specified scope.
1379
+ *
1380
+ * It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
1381
+ */
1382
+ const useClient = (scope) => {
1383
+ return useInject(__alepha_server_links.LinkProvider).client(scope);
1336
1384
  };
1337
1385
 
1338
1386
  //#endregion
1339
1387
  //#region src/hooks/useQueryParams.ts
1388
+ /**
1389
+ * Not well tested. Use with caution.
1390
+ */
1340
1391
  const useQueryParams = (schema, options = {}) => {
1341
1392
  const alepha = useAlepha();
1342
1393
  const key = options.key ?? "q";
@@ -1367,29 +1418,17 @@ const decode = (alepha, schema, data) => {
1367
1418
  }
1368
1419
  };
1369
1420
 
1370
- //#endregion
1371
- //#region src/hooks/useRouterState.ts
1372
- const useRouterState = () => {
1373
- const router = (0, react.useContext)(RouterContext);
1374
- const layer = (0, react.useContext)(RouterLayerContext);
1375
- if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1376
- const [state, setState] = (0, react.useState)(router.state);
1377
- useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1378
- return state;
1379
- };
1380
-
1381
1421
  //#endregion
1382
1422
  //#region src/hooks/useSchema.ts
1383
1423
  const useSchema = (action) => {
1384
1424
  const name = action.name;
1385
1425
  const alepha = useAlepha();
1386
1426
  const httpClient = useInject(__alepha_server.HttpClient);
1387
- const linkProvider = useInject(__alepha_server_links.LinkProvider);
1388
1427
  const [schema, setSchema] = (0, react.useState)(ssrSchemaLoading(alepha, name));
1389
1428
  (0, react.useEffect)(() => {
1390
1429
  if (!schema.loading) return;
1391
1430
  const opts = { cache: true };
1392
- httpClient.fetch(`${linkProvider.URL_LINKS}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1431
+ httpClient.fetch(`${__alepha_server_links.LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1393
1432
  }, [name]);
1394
1433
  return schema;
1395
1434
  };
@@ -1398,10 +1437,10 @@ const useSchema = (action) => {
1398
1437
  */
1399
1438
  const ssrSchemaLoading = (alepha, name) => {
1400
1439
  if (!alepha.isBrowser()) {
1401
- const links = alepha.context.get("links")?.links ?? [];
1402
- const can = links.find((it) => it.name === name);
1440
+ const linkProvider = alepha.inject(__alepha_server_links.LinkProvider);
1441
+ const can = linkProvider.getServerLinks().find((link) => link.name === name);
1403
1442
  if (can) {
1404
- const schema$1 = alepha.inject(__alepha_server_links.LinkProvider).links?.find((it) => it.name === name)?.schema;
1443
+ const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
1405
1444
  if (schema$1) {
1406
1445
  can.schema = schema$1;
1407
1446
  return schema$1;
@@ -1409,7 +1448,7 @@ const ssrSchemaLoading = (alepha, name) => {
1409
1448
  }
1410
1449
  return { loading: true };
1411
1450
  }
1412
- const schema = alepha.inject(__alepha_server_links.LinkProvider).links?.find((it) => it.name === name)?.schema;
1451
+ const schema = alepha.inject(__alepha_server_links.LinkProvider).links.find((it) => it.name === name)?.schema;
1413
1452
  if (schema) return schema;
1414
1453
  return { loading: true };
1415
1454
  };
@@ -1431,10 +1470,10 @@ const AlephaReact = (0, __alepha_core.$module)({
1431
1470
  descriptors: [$page],
1432
1471
  services: [
1433
1472
  ReactServerProvider,
1434
- PageDescriptorProvider,
1435
- ReactBrowserProvider
1473
+ ReactPageProvider,
1474
+ ReactRouter
1436
1475
  ],
1437
- register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider)
1476
+ register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
1438
1477
  });
1439
1478
 
1440
1479
  //#endregion
@@ -1443,16 +1482,16 @@ exports.AlephaContext = AlephaContext;
1443
1482
  exports.AlephaReact = AlephaReact;
1444
1483
  exports.ClientOnly = ClientOnly_default;
1445
1484
  exports.ErrorBoundary = ErrorBoundary_default;
1485
+ exports.ErrorViewer = ErrorViewer_default;
1446
1486
  exports.Link = Link_default;
1447
1487
  exports.NestedView = NestedView_default;
1448
1488
  exports.NotFound = NotFoundPage;
1449
1489
  exports.PageDescriptor = PageDescriptor;
1450
- exports.PageDescriptorProvider = PageDescriptorProvider;
1451
1490
  exports.ReactBrowserProvider = ReactBrowserProvider;
1491
+ exports.ReactPageProvider = ReactPageProvider;
1492
+ exports.ReactRouter = ReactRouter;
1452
1493
  exports.ReactServerProvider = ReactServerProvider;
1453
1494
  exports.Redirection = Redirection;
1454
- exports.RouterContext = RouterContext;
1455
- exports.RouterHookApi = RouterHookApi;
1456
1495
  exports.RouterLayerContext = RouterLayerContext;
1457
1496
  exports.isPageRoute = isPageRoute;
1458
1497
  exports.ssrSchemaLoading = ssrSchemaLoading;