@alepha/react 0.9.2 → 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 (40) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +378 -325
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +570 -458
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +305 -213
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +304 -212
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +567 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +16 -13
  13. package/src/components/ErrorViewer.tsx +1 -1
  14. package/src/components/Link.tsx +4 -24
  15. package/src/components/NestedView.tsx +20 -9
  16. package/src/components/NotFound.tsx +5 -2
  17. package/src/descriptors/$page.ts +86 -12
  18. package/src/errors/Redirection.ts +13 -0
  19. package/src/hooks/useActive.ts +28 -30
  20. package/src/hooks/useAlepha.ts +16 -2
  21. package/src/hooks/useClient.ts +7 -2
  22. package/src/hooks/useInject.ts +4 -1
  23. package/src/hooks/useQueryParams.ts +9 -6
  24. package/src/hooks/useRouter.ts +18 -30
  25. package/src/hooks/useRouterEvents.ts +7 -4
  26. package/src/hooks/useRouterState.ts +8 -20
  27. package/src/hooks/useSchema.ts +10 -15
  28. package/src/hooks/useStore.ts +9 -8
  29. package/src/index.browser.ts +11 -11
  30. package/src/index.shared.ts +4 -5
  31. package/src/index.ts +21 -30
  32. package/src/providers/ReactBrowserProvider.ts +155 -65
  33. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  34. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
  35. package/src/providers/ReactServerProvider.ts +100 -68
  36. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
  37. package/src/contexts/RouterContext.ts +0 -14
  38. package/src/errors/RedirectionError.ts +0 -10
  39. package/src/providers/BrowserRouterProvider.ts +0 -146
  40. 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, NotImplementedError, 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
  /**
@@ -18,6 +21,12 @@ const $page = (options) => {
18
21
  return createDescriptor(PageDescriptor, options);
19
22
  };
20
23
  var PageDescriptor = class extends Descriptor {
24
+ onInit() {
25
+ if (this.options.static) this.options.cache ??= {
26
+ provider: "memory",
27
+ ttl: [1, "week"]
28
+ };
29
+ }
21
30
  get name() {
22
31
  return this.options.name ?? this.config.propertyKey;
23
32
  }
@@ -26,7 +35,13 @@ var PageDescriptor = class extends Descriptor {
26
35
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
27
36
  */
28
37
  async render(options) {
29
- throw new NotImplementedError("");
38
+ throw new Error("render method is not implemented in this environment");
39
+ }
40
+ match(url) {
41
+ return false;
42
+ }
43
+ pathname(config) {
44
+ return this.options.path || "";
30
45
  }
31
46
  };
32
47
  $page[KIND] = PageDescriptor;
@@ -80,7 +95,7 @@ const ErrorViewer = ({ error, alepha }) => {
80
95
  heading: {
81
96
  fontSize: "20px",
82
97
  fontWeight: "bold",
83
- marginBottom: "4px"
98
+ marginBottom: "10px"
84
99
  },
85
100
  name: {
86
101
  fontSize: "16px",
@@ -199,28 +214,54 @@ const ErrorViewerProduction = () => {
199
214
  });
200
215
  };
201
216
 
202
- //#endregion
203
- //#region src/contexts/RouterContext.ts
204
- const RouterContext = createContext(void 0);
205
-
206
217
  //#endregion
207
218
  //#region src/contexts/RouterLayerContext.ts
208
219
  const RouterLayerContext = createContext(void 0);
209
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
+
210
236
  //#endregion
211
237
  //#region src/contexts/AlephaContext.ts
212
238
  const AlephaContext = createContext(void 0);
213
239
 
214
240
  //#endregion
215
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
+ */
216
254
  const useAlepha = () => {
217
255
  const alepha = useContext(AlephaContext);
218
- 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");
219
257
  return alepha;
220
258
  };
221
259
 
222
260
  //#endregion
223
261
  //#region src/hooks/useRouterEvents.ts
262
+ /**
263
+ * Subscribe to various router events.
264
+ */
224
265
  const useRouterEvents = (opts = {}, deps = []) => {
225
266
  const alepha = useAlepha();
226
267
  useEffect(() => {
@@ -293,17 +334,22 @@ var ErrorBoundary_default = ErrorBoundary;
293
334
  * ```
294
335
  */
295
336
  const NestedView = (props) => {
296
- const app = useContext(RouterContext);
297
337
  const layer = useContext(RouterLayerContext);
298
338
  const index = layer?.index ?? 0;
299
- const [view, setView] = useState(app?.state.layers[index]?.element);
300
- useRouterEvents({ onEnd: ({ state }) => {
301
- if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
302
- } }, [app]);
303
- 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
+ } }, []);
304
346
  const element = view ?? props.children ?? null;
305
347
  return /* @__PURE__ */ jsx(ErrorBoundary_default, {
306
- fallback: app.context.onError,
348
+ fallback: (error) => {
349
+ const result = state.onError(error, state);
350
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
351
+ return result;
352
+ },
307
353
  children: element
308
354
  });
309
355
  };
@@ -311,7 +357,7 @@ var NestedView_default = NestedView;
311
357
 
312
358
  //#endregion
313
359
  //#region src/components/NotFound.tsx
314
- function NotFoundPage() {
360
+ function NotFoundPage(props) {
315
361
  return /* @__PURE__ */ jsx("div", {
316
362
  style: {
317
363
  height: "100vh",
@@ -321,34 +367,25 @@ function NotFoundPage() {
321
367
  alignItems: "center",
322
368
  textAlign: "center",
323
369
  fontFamily: "sans-serif",
324
- padding: "1rem"
370
+ padding: "1rem",
371
+ ...props.style
325
372
  },
326
373
  children: /* @__PURE__ */ jsx("h1", {
327
374
  style: {
328
375
  fontSize: "1rem",
329
376
  marginBottom: "0.5rem"
330
377
  },
331
- children: "This page does not exist"
378
+ children: "404 - This page does not exist"
332
379
  })
333
380
  });
334
381
  }
335
382
 
336
383
  //#endregion
337
- //#region src/errors/RedirectionError.ts
338
- var RedirectionError = class extends Error {
339
- page;
340
- constructor(page) {
341
- super("Redirection");
342
- this.page = page;
343
- }
344
- };
345
-
346
- //#endregion
347
- //#region src/providers/PageDescriptorProvider.ts
348
- const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
349
- 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 {
350
387
  log = $logger();
351
- env = $env(envSchema$1);
388
+ env = $env(envSchema$2);
352
389
  alepha = $inject(Alepha);
353
390
  pages = [];
354
391
  getPages() {
@@ -358,7 +395,7 @@ var PageDescriptorProvider = class {
358
395
  for (const page of this.pages) if (page.name === name) return page;
359
396
  throw new Error(`Page ${name} not found`);
360
397
  }
361
- url(name, options = {}) {
398
+ pathname(name, options = {}) {
362
399
  const page = this.page(name);
363
400
  if (!page) throw new Error(`Page ${name} not found`);
364
401
  let url = page.path ?? "";
@@ -368,22 +405,28 @@ var PageDescriptorProvider = class {
368
405
  parent = parent.parent;
369
406
  }
370
407
  url = this.compile(url, options.params ?? {});
371
- return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
408
+ if (options.query) {
409
+ const query = new URLSearchParams(options.query);
410
+ if (query.toString()) url += `?${query.toString()}`;
411
+ }
412
+ return url.replace(/\/\/+/g, "/") || "/";
372
413
  }
373
- root(state, context) {
374
- const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(RouterContext.Provider, { value: {
375
- state,
376
- context
377
- } }, createElement(NestedView_default, {}, state.layers[0]?.element)));
414
+ url(name, options = {}) {
415
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
416
+ }
417
+ root(state) {
418
+ const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
378
419
  if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
379
420
  return root;
380
421
  }
381
- async createLayers(route, request) {
382
- const { pathname, search } = request.url;
383
- 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 = []) {
384
428
  let context = {};
385
429
  const stack = [{ route }];
386
- request.onError = (error) => this.renderError(error);
387
430
  let parent = route.parent;
388
431
  while (parent) {
389
432
  stack.unshift({ route: parent });
@@ -395,19 +438,18 @@ var PageDescriptorProvider = class {
395
438
  const route$1 = it.route;
396
439
  const config = {};
397
440
  try {
398
- 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) : {};
399
442
  } catch (e) {
400
443
  it.error = e;
401
444
  break;
402
445
  }
403
446
  try {
404
- 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) : {};
405
448
  } catch (e) {
406
449
  it.error = e;
407
450
  break;
408
451
  }
409
452
  it.config = { ...config };
410
- const previous = request.previous;
411
453
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
412
454
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
413
455
  const prev = JSON.stringify({
@@ -433,7 +475,7 @@ var PageDescriptorProvider = class {
433
475
  if (!route$1.resolve) continue;
434
476
  try {
435
477
  const props = await route$1.resolve?.({
436
- ...request,
478
+ ...state,
437
479
  ...config,
438
480
  ...context
439
481
  }) ?? {};
@@ -443,13 +485,8 @@ var PageDescriptorProvider = class {
443
485
  ...props
444
486
  };
445
487
  } catch (e) {
446
- if (e instanceof RedirectionError) return {
447
- layers: [],
448
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
449
- pathname,
450
- search
451
- };
452
- this.log.error(e);
488
+ if (e instanceof Redirection) return { redirect: e.redirect };
489
+ this.log.error("Page resolver has failed", e);
453
490
  it.error = e;
454
491
  break;
455
492
  }
@@ -464,44 +501,59 @@ var PageDescriptorProvider = class {
464
501
  acc += it.route.path ? this.compile(it.route.path, params) : "";
465
502
  const path = acc.replace(/\/+/, "/");
466
503
  const localErrorHandler = this.getErrorHandler(it.route);
467
- if (localErrorHandler) request.onError = localErrorHandler;
468
- if (it.error) {
469
- let element$1 = await request.onError(it.error);
470
- if (element$1 === null) element$1 = this.renderError(it.error);
471
- layers.push({
504
+ if (localErrorHandler) {
505
+ const onErrorParent = state.onError;
506
+ state.onError = (error, context$1) => {
507
+ const result = localErrorHandler(error, context$1);
508
+ if (result === void 0) return onErrorParent(error, context$1);
509
+ return result;
510
+ };
511
+ }
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
527
+ });
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({
472
537
  props,
473
538
  error: it.error,
474
539
  name: it.route.name,
475
540
  part: it.route.path,
476
541
  config: it.config,
477
- element: this.renderView(i + 1, path, element$1, it.route),
542
+ element: this.renderView(i + 1, path, element, it.route),
478
543
  index: i + 1,
479
544
  path,
480
545
  route: it.route
481
546
  });
482
547
  break;
548
+ } catch (e) {
549
+ if (e instanceof Redirection) return { redirect: e.redirect };
550
+ throw e;
483
551
  }
484
- const element = await this.createElement(it.route, {
485
- ...props,
486
- ...context
487
- });
488
- layers.push({
489
- name: it.route.name,
490
- props,
491
- part: it.route.path,
492
- config: it.config,
493
- element: this.renderView(i + 1, path, element, it.route),
494
- index: i + 1,
495
- path,
496
- route: it.route,
497
- cache: it.cache
498
- });
499
552
  }
500
- return {
501
- layers,
502
- pathname,
503
- search
504
- };
553
+ return { state };
554
+ }
555
+ createRedirectionLayer(redirect) {
556
+ return { redirect };
505
557
  }
506
558
  getErrorHandler(route) {
507
559
  if (route.errorHandler) return route.errorHandler;
@@ -558,6 +610,7 @@ var PageDescriptorProvider = class {
558
610
  let hasNotFoundHandler = false;
559
611
  const pages = this.alepha.descriptors($page);
560
612
  const hasParent = (it) => {
613
+ if (it.options.parent) return true;
561
614
  for (const page of pages) {
562
615
  const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
563
616
  if (children.includes(it)) return true;
@@ -573,7 +626,7 @@ var PageDescriptorProvider = class {
573
626
  name: "notFound",
574
627
  cache: true,
575
628
  component: NotFoundPage,
576
- afterHandler: ({ reply }) => {
629
+ onServerResponse: ({ reply }) => {
577
630
  reply.status = 404;
578
631
  }
579
632
  });
@@ -581,6 +634,12 @@ var PageDescriptorProvider = class {
581
634
  });
582
635
  map(pages, target) {
583
636
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
637
+ const getChildrenFromParent = (it) => {
638
+ const children$1 = [];
639
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
640
+ return children$1;
641
+ };
642
+ children.push(...getChildrenFromParent(target));
584
643
  return {
585
644
  ...target.options,
586
645
  name: target.name,
@@ -620,217 +679,9 @@ const isPageRoute = (it) => {
620
679
  return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
621
680
  };
622
681
 
623
- //#endregion
624
- //#region src/providers/BrowserRouterProvider.ts
625
- var BrowserRouterProvider = class extends RouterProvider {
626
- log = $logger();
627
- alepha = $inject(Alepha);
628
- pageDescriptorProvider = $inject(PageDescriptorProvider);
629
- add(entry) {
630
- this.pageDescriptorProvider.add(entry);
631
- }
632
- configure = $hook({
633
- on: "configure",
634
- handler: async () => {
635
- for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
636
- path: page.match,
637
- page
638
- });
639
- }
640
- });
641
- async transition(url, options = {}) {
642
- const { pathname, search } = url;
643
- const state = {
644
- pathname,
645
- search,
646
- layers: []
647
- };
648
- const context = {
649
- url,
650
- query: {},
651
- params: {},
652
- onError: () => null,
653
- ...options.context ?? {}
654
- };
655
- await this.alepha.emit("react:transition:begin", {
656
- state,
657
- context
658
- });
659
- try {
660
- const previous = options.previous;
661
- const { route, params } = this.match(pathname);
662
- const query = {};
663
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
664
- context.query = query;
665
- context.params = params ?? {};
666
- context.previous = previous;
667
- if (isPageRoute(route)) {
668
- const result = await this.pageDescriptorProvider.createLayers(route.page, context);
669
- if (result.redirect) return {
670
- redirect: result.redirect,
671
- state,
672
- context
673
- };
674
- state.layers = result.layers;
675
- }
676
- if (state.layers.length === 0) state.layers.push({
677
- name: "not-found",
678
- element: createElement(NotFoundPage),
679
- index: 0,
680
- path: "/"
681
- });
682
- await this.alepha.emit("react:transition:success", {
683
- state,
684
- context
685
- });
686
- } catch (e) {
687
- this.log.error(e);
688
- state.layers = [{
689
- name: "error",
690
- element: this.pageDescriptorProvider.renderError(e),
691
- index: 0,
692
- path: "/"
693
- }];
694
- await this.alepha.emit("react:transition:error", {
695
- error: e,
696
- state,
697
- context
698
- });
699
- }
700
- if (options.state) {
701
- options.state.layers = state.layers;
702
- options.state.pathname = state.pathname;
703
- options.state.search = state.search;
704
- }
705
- await this.alepha.emit("react:transition:end", {
706
- state: options.state,
707
- context
708
- });
709
- return {
710
- context,
711
- state
712
- };
713
- }
714
- root(state, context) {
715
- return this.pageDescriptorProvider.root(state, context);
716
- }
717
- };
718
-
719
- //#endregion
720
- //#region src/providers/ReactBrowserProvider.ts
721
- var ReactBrowserProvider = class {
722
- log = $logger();
723
- client = $inject(LinkProvider);
724
- alepha = $inject(Alepha);
725
- router = $inject(BrowserRouterProvider);
726
- root;
727
- transitioning;
728
- state = {
729
- layers: [],
730
- pathname: "",
731
- search: ""
732
- };
733
- get document() {
734
- return window.document;
735
- }
736
- get history() {
737
- return window.history;
738
- }
739
- get location() {
740
- return window.location;
741
- }
742
- get url() {
743
- let url = this.location.pathname + this.location.search;
744
- if (import.meta?.env?.BASE_URL) {
745
- url = url.replace(import.meta.env?.BASE_URL, "");
746
- if (!url.startsWith("/")) url = `/${url}`;
747
- }
748
- return url;
749
- }
750
- pushState(url, replace) {
751
- let path = url;
752
- if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
753
- if (replace) this.history.replaceState({}, "", path);
754
- else this.history.pushState({}, "", path);
755
- }
756
- async invalidate(props) {
757
- const previous = [];
758
- if (props) {
759
- const [key] = Object.keys(props);
760
- const value = props[key];
761
- for (const layer of this.state.layers) {
762
- if (layer.props?.[key]) {
763
- previous.push({
764
- ...layer,
765
- props: {
766
- ...layer.props,
767
- [key]: value
768
- }
769
- });
770
- break;
771
- }
772
- previous.push(layer);
773
- }
774
- }
775
- await this.render({ previous });
776
- }
777
- async go(url, options = {}) {
778
- const result = await this.render({ url });
779
- if (result.context.url.pathname !== url) {
780
- this.pushState(result.context.url.pathname);
781
- return;
782
- }
783
- if (options.replace) {
784
- this.pushState(url);
785
- return;
786
- }
787
- this.pushState(url);
788
- }
789
- async render(options = {}) {
790
- const previous = options.previous ?? this.state.layers;
791
- const url = options.url ?? this.url;
792
- this.transitioning = { to: url };
793
- const result = await this.router.transition(new URL(`http://localhost${url}`), {
794
- previous,
795
- state: this.state
796
- });
797
- if (result.redirect) return await this.render({ url: result.redirect });
798
- this.transitioning = void 0;
799
- return result;
800
- }
801
- /**
802
- * Get embedded layers from the server.
803
- */
804
- getHydrationState() {
805
- try {
806
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
807
- } catch (error) {
808
- console.error(error);
809
- }
810
- }
811
- ready = $hook({
812
- on: "ready",
813
- handler: async () => {
814
- const hydration = this.getHydrationState();
815
- const previous = hydration?.layers ?? [];
816
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
817
- const { context } = await this.render({ previous });
818
- await this.alepha.emit("react:browser:render", {
819
- state: this.state,
820
- context,
821
- hydration
822
- });
823
- window.addEventListener("popstate", () => {
824
- if (this.state.pathname === this.url) return;
825
- this.render();
826
- });
827
- }
828
- });
829
- };
830
-
831
682
  //#endregion
832
683
  //#region src/providers/ReactServerProvider.ts
833
- const envSchema = t.object({
684
+ const envSchema$1 = t.object({
834
685
  REACT_SERVER_DIST: t.string({ default: "public" }),
835
686
  REACT_SERVER_PREFIX: t.string({ default: "" }),
836
687
  REACT_SSR_ENABLED: t.optional(t.boolean()),
@@ -840,11 +691,11 @@ const envSchema = t.object({
840
691
  var ReactServerProvider = class {
841
692
  log = $logger();
842
693
  alepha = $inject(Alepha);
843
- pageDescriptorProvider = $inject(PageDescriptorProvider);
694
+ pageApi = $inject(ReactPageProvider);
844
695
  serverStaticProvider = $inject(ServerStaticProvider);
845
696
  serverRouterProvider = $inject(ServerRouterProvider);
846
697
  serverTimingProvider = $inject(ServerTimingProvider);
847
- env = $env(envSchema);
698
+ env = $env(envSchema$1);
848
699
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
849
700
  onConfigure = $hook({
850
701
  on: "configure",
@@ -891,7 +742,7 @@ var ReactServerProvider = class {
891
742
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
892
743
  }
893
744
  async registerPages(templateLoader) {
894
- for (const page of this.pageDescriptorProvider.getPages()) {
745
+ for (const page of this.pageApi.getPages()) {
895
746
  if (page.children?.length) continue;
896
747
  this.log.debug(`+ ${page.match} -> ${page.name}`);
897
748
  this.serverRouterProvider.createRoute({
@@ -925,24 +776,30 @@ var ReactServerProvider = class {
925
776
  */
926
777
  createRenderFunction(name, withIndex = false) {
927
778
  return async (options = {}) => {
928
- const page = this.pageDescriptorProvider.page(name);
929
- const url = new URL(this.pageDescriptorProvider.url(name, options));
930
- const context = {
779
+ const page = this.pageApi.page(name);
780
+ const url = new URL(this.pageApi.url(name, options));
781
+ const entry = {
931
782
  url,
932
783
  params: options.params ?? {},
933
784
  query: options.query ?? {},
934
- head: {},
935
- onError: () => null
785
+ onError: () => null,
786
+ layers: []
936
787
  };
937
- await this.alepha.emit("react:server:render:begin", { context });
938
- const state = await this.pageDescriptorProvider.createLayers(page, context);
939
- if (!withIndex && !options.html) return {
940
- context,
941
- html: renderToString(this.pageDescriptorProvider.root(state, context))
942
- };
943
- 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);
801
+ if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
944
802
  const result = {
945
- context,
946
803
  state,
947
804
  html
948
805
  };
@@ -950,30 +807,27 @@ var ReactServerProvider = class {
950
807
  return result;
951
808
  };
952
809
  }
953
- createHandler(page, templateLoader) {
810
+ createHandler(route, templateLoader) {
954
811
  return async (serverRequest) => {
955
812
  const { url, reply, query, params } = serverRequest;
956
813
  const template = await templateLoader();
957
814
  if (!template) throw new Error("Template not found");
958
- const context = {
815
+ this.log.trace("Rendering page", { name: route.name });
816
+ const entry = {
959
817
  url,
960
818
  params,
961
819
  query,
962
- head: {},
963
- onError: () => null
820
+ onError: () => null,
821
+ layers: []
964
822
  };
965
- if (this.alepha.has(ServerLinksProvider)) {
966
- const srv = this.alepha.inject(ServerLinksProvider);
967
- const schema = apiLinksResponseSchema;
968
- context.links = this.alepha.parse(schema, await srv.getLinks({
969
- user: serverRequest.user,
970
- authorization: serverRequest.headers.authorization
971
- }));
972
- this.alepha.context.set("links", context.links);
973
- }
974
- 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;
975
829
  while (target) {
976
- if (page.can && !page.can()) {
830
+ if (route.can && !route.can()) {
977
831
  reply.status = 403;
978
832
  reply.headers["content-type"] = "text/plain";
979
833
  return "Forbidden";
@@ -982,50 +836,60 @@ var ReactServerProvider = class {
982
836
  }
983
837
  await this.alepha.emit("react:server:render:begin", {
984
838
  request: serverRequest,
985
- context
839
+ state
986
840
  });
987
841
  this.serverTimingProvider.beginTiming("createLayers");
988
- const state = await this.pageDescriptorProvider.createLayers(page, context);
842
+ const { redirect } = await this.pageApi.createLayers(route, state);
989
843
  this.serverTimingProvider.endTiming("createLayers");
990
- if (state.redirect) return reply.redirect(state.redirect);
844
+ if (redirect) return reply.redirect(redirect);
991
845
  reply.headers["content-type"] = "text/html";
992
846
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
993
847
  reply.headers.pragma = "no-cache";
994
848
  reply.headers.expires = "0";
995
- if (page.cache && serverRequest.user) delete context.links;
996
- const html = this.renderToHtml(template, state, context);
997
- await this.alepha.emit("react:server:render:end", {
849
+ const html = this.renderToHtml(template, state);
850
+ if (html instanceof Redirection) {
851
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
852
+ return;
853
+ }
854
+ const event = {
998
855
  request: serverRequest,
999
- context,
1000
856
  state,
1001
857
  html
1002
- });
1003
- page.afterHandler?.(serverRequest);
1004
- return html;
858
+ };
859
+ await this.alepha.emit("react:server:render:end", event);
860
+ route.onServerResponse?.(serverRequest);
861
+ this.log.trace("Page rendered", { name: route.name });
862
+ return event.html;
1005
863
  };
1006
864
  }
1007
- renderToHtml(template, state, context, hydration = true) {
1008
- 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);
1009
868
  this.serverTimingProvider.beginTiming("renderToString");
1010
869
  let app = "";
1011
870
  try {
1012
871
  app = renderToString(element);
1013
872
  } catch (error) {
1014
- this.log.error("Error during SSR", error);
1015
- app = renderToString(context.onError(error));
873
+ this.log.error("renderToString has failed, fallback to error handler", error);
874
+ const element$1 = state.onError(error, state);
875
+ if (element$1 instanceof Redirection) return element$1;
876
+ app = renderToString(element$1);
877
+ this.log.debug("Error handled successfully with fallback");
1016
878
  }
1017
879
  this.serverTimingProvider.endTiming("renderToString");
1018
880
  const response = { html: template };
1019
881
  if (hydration) {
882
+ const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
1020
883
  const hydrationData = {
1021
- links: context.links,
884
+ ...store,
885
+ "react.router.state": void 0,
1022
886
  layers: state.layers.map((it) => ({
1023
887
  ...it,
1024
888
  error: it.error ? {
1025
889
  ...it.error,
1026
890
  name: it.error.name,
1027
891
  message: it.error.message,
1028
- stack: it.error.stack
892
+ stack: !this.alepha.isProduction() ? it.error.stack : void 0
1029
893
  } : void 0,
1030
894
  index: void 0,
1031
895
  path: void 0,
@@ -1033,7 +897,7 @@ var ReactServerProvider = class {
1033
897
  route: void 0
1034
898
  }))
1035
899
  };
1036
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
900
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1037
901
  this.fillTemplate(response, app, script);
1038
902
  }
1039
903
  return response.html;
@@ -1045,26 +909,269 @@ var ReactServerProvider = class {
1045
909
  else {
1046
910
  const bodyOpenTag = /<body([^>]*)>/i;
1047
911
  if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1048
- return `${match}\n<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
912
+ return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1049
913
  });
1050
914
  }
1051
915
  const bodyCloseTagRegex = /<\/body>/i;
1052
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}\n</body>`);
916
+ if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
917
+ }
918
+ };
919
+
920
+ //#endregion
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);
1053
988
  }
1054
989
  };
1055
990
 
1056
991
  //#endregion
1057
- //#region src/hooks/RouterHookApi.ts
1058
- var RouterHookApi = class {
1059
- constructor(pages, context, state, layer, browser) {
1060
- this.pages = pages;
1061
- this.context = context;
1062
- this.state = state;
1063
- this.layer = layer;
1064
- this.browser = browser;
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;
1163
+ }
1164
+ path(name, config = {}) {
1165
+ return this.pageApi.pathname(name, {
1166
+ params: {
1167
+ ...this.state.params,
1168
+ ...config.params
1169
+ },
1170
+ query: config.query
1171
+ });
1065
1172
  }
1066
1173
  getURL() {
1067
- if (!this.browser) return this.context.url;
1174
+ if (!this.browser) return this.state.url;
1068
1175
  return new URL(this.location.href);
1069
1176
  }
1070
1177
  get location() {
@@ -1075,11 +1182,11 @@ var RouterHookApi = class {
1075
1182
  return this.state;
1076
1183
  }
1077
1184
  get pathname() {
1078
- return this.state.pathname;
1185
+ return this.state.url.pathname;
1079
1186
  }
1080
1187
  get query() {
1081
1188
  const query = {};
1082
- 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);
1083
1190
  return query;
1084
1191
  }
1085
1192
  async back() {
@@ -1091,39 +1198,33 @@ var RouterHookApi = class {
1091
1198
  async invalidate(props) {
1092
1199
  await this.browser?.invalidate(props);
1093
1200
  }
1094
- /**
1095
- * Create a valid href for the given pathname.
1096
- *
1097
- * @param pathname
1098
- * @param layer
1099
- */
1100
- createHref(pathname, layer = this.layer, options = {}) {
1101
- if (typeof pathname === "object") pathname = pathname.options.path ?? "";
1102
- if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
1103
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
1104
- }
1105
1201
  async go(path, options) {
1106
1202
  for (const page of this.pages) if (page.name === path) {
1107
- path = page.path ?? "";
1108
- break;
1203
+ await this.browser?.go(this.path(path, options), options);
1204
+ return;
1109
1205
  }
1110
- await this.browser?.go(this.createHref(path, this.layer, options), options);
1206
+ await this.browser?.go(path, options);
1111
1207
  }
1112
1208
  anchor(path, options = {}) {
1209
+ let href = path;
1113
1210
  for (const page of this.pages) if (page.name === path) {
1114
- path = page.path ?? "";
1211
+ href = this.path(path, options);
1115
1212
  break;
1116
1213
  }
1117
- const href = this.createHref(path, this.layer, options);
1118
1214
  return {
1119
- href,
1215
+ href: this.base(href),
1120
1216
  onClick: (ev) => {
1121
1217
  ev.stopPropagation();
1122
1218
  ev.preventDefault();
1123
- this.go(path, options).catch(console.error);
1219
+ this.go(href, options).catch(console.error);
1124
1220
  }
1125
1221
  };
1126
1222
  }
1223
+ base(path) {
1224
+ const base = import.meta.env?.BASE_URL;
1225
+ if (!base || base === "/") return path;
1226
+ return base + path;
1227
+ }
1127
1228
  /**
1128
1229
  * Set query params.
1129
1230
  *
@@ -1139,90 +1240,131 @@ var RouterHookApi = class {
1139
1240
  }
1140
1241
  };
1141
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
+
1142
1254
  //#endregion
1143
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
+ */
1144
1270
  const useRouter = () => {
1145
- const alepha = useAlepha();
1146
- const ctx = useContext(RouterContext);
1147
- const layer = useContext(RouterLayerContext);
1148
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1149
- const pages = useMemo(() => {
1150
- return alepha.inject(PageDescriptorProvider).getPages();
1151
- }, []);
1152
- return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1271
+ return useInject(ReactRouter);
1153
1272
  };
1154
1273
 
1155
1274
  //#endregion
1156
1275
  //#region src/components/Link.tsx
1157
1276
  const Link = (props) => {
1158
- React.useContext(RouterContext);
1159
1277
  const router = useRouter();
1160
- const to = typeof props.to === "string" ? props.to : props.to.options.path;
1161
- if (!to) return null;
1162
- const can = typeof props.to === "string" ? void 0 : props.to.options.can;
1163
- if (can && !can()) return null;
1164
- const name = typeof props.to === "string" ? void 0 : props.to.options.name;
1165
- const anchorProps = {
1166
- ...props,
1167
- to: void 0
1168
- };
1278
+ const { to,...anchorProps } = props;
1169
1279
  return /* @__PURE__ */ jsx("a", {
1170
1280
  ...router.anchor(to),
1171
1281
  ...anchorProps,
1172
- children: props.children ?? name
1282
+ children: props.children
1173
1283
  });
1174
1284
  };
1175
1285
  var Link_default = Link;
1176
1286
 
1287
+ //#endregion
1288
+ //#region src/hooks/useStore.ts
1289
+ /**
1290
+ * Hook to access and mutate the Alepha state.
1291
+ */
1292
+ const useStore = (key, defaultValue) => {
1293
+ const alepha = useAlepha();
1294
+ useMemo(() => {
1295
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1296
+ }, [defaultValue]);
1297
+ const [state, setState] = useState(alepha.state(key));
1298
+ useEffect(() => {
1299
+ if (!alepha.isBrowser()) return;
1300
+ return alepha.on("state:mutate", (ev) => {
1301
+ if (ev.key === key) setState(ev.value);
1302
+ });
1303
+ }, []);
1304
+ return [state, (value) => {
1305
+ alepha.state(key, value);
1306
+ }];
1307
+ };
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
+
1177
1317
  //#endregion
1178
1318
  //#region src/hooks/useActive.ts
1179
- const useActive = (path) => {
1319
+ const useActive = (args) => {
1180
1320
  const router = useRouter();
1181
- const ctx = useContext(RouterContext);
1182
- const layer = useContext(RouterLayerContext);
1183
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1184
- let name;
1185
- if (typeof path === "object" && path.options.name) name = path.options.name;
1186
- const [current, setCurrent] = useState(ctx.state.pathname);
1187
- const href = useMemo(() => router.createHref(path, layer), [path, layer]);
1188
1321
  const [isPending, setPending] = useState(false);
1189
- const isActive = current === href;
1190
- useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
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);
1191
1331
  return {
1192
- name,
1193
1332
  isPending,
1194
1333
  isActive,
1195
1334
  anchorProps: {
1196
- href,
1197
- onClick: (ev) => {
1198
- ev.stopPropagation();
1199
- ev.preventDefault();
1335
+ href: router.base(href),
1336
+ onClick: async (ev) => {
1337
+ ev?.stopPropagation();
1338
+ ev?.preventDefault();
1200
1339
  if (isActive) return;
1201
1340
  if (isPending) return;
1202
1341
  setPending(true);
1203
- router.go(href).then(() => {
1342
+ try {
1343
+ await router.go(href);
1344
+ } finally {
1204
1345
  setPending(false);
1205
- });
1346
+ }
1206
1347
  }
1207
1348
  }
1208
1349
  };
1209
1350
  };
1210
1351
 
1211
- //#endregion
1212
- //#region src/hooks/useInject.ts
1213
- const useInject = (service) => {
1214
- const alepha = useAlepha();
1215
- return useMemo(() => alepha.inject(service), []);
1216
- };
1217
-
1218
1352
  //#endregion
1219
1353
  //#region src/hooks/useClient.ts
1220
- const useClient = (_scope) => {
1221
- 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);
1222
1361
  };
1223
1362
 
1224
1363
  //#endregion
1225
1364
  //#region src/hooks/useQueryParams.ts
1365
+ /**
1366
+ * Not well tested. Use with caution.
1367
+ */
1226
1368
  const useQueryParams = (schema, options = {}) => {
1227
1369
  const alepha = useAlepha();
1228
1370
  const key = options.key ?? "q";
@@ -1253,29 +1395,17 @@ const decode = (alepha, schema, data) => {
1253
1395
  }
1254
1396
  };
1255
1397
 
1256
- //#endregion
1257
- //#region src/hooks/useRouterState.ts
1258
- const useRouterState = () => {
1259
- const router = useContext(RouterContext);
1260
- const layer = useContext(RouterLayerContext);
1261
- if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1262
- const [state, setState] = useState(router.state);
1263
- useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1264
- return state;
1265
- };
1266
-
1267
1398
  //#endregion
1268
1399
  //#region src/hooks/useSchema.ts
1269
1400
  const useSchema = (action) => {
1270
1401
  const name = action.name;
1271
1402
  const alepha = useAlepha();
1272
1403
  const httpClient = useInject(HttpClient);
1273
- const linkProvider = useInject(LinkProvider);
1274
1404
  const [schema, setSchema] = useState(ssrSchemaLoading(alepha, name));
1275
1405
  useEffect(() => {
1276
1406
  if (!schema.loading) return;
1277
1407
  const opts = { cache: true };
1278
- 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));
1279
1409
  }, [name]);
1280
1410
  return schema;
1281
1411
  };
@@ -1284,10 +1414,10 @@ const useSchema = (action) => {
1284
1414
  */
1285
1415
  const ssrSchemaLoading = (alepha, name) => {
1286
1416
  if (!alepha.isBrowser()) {
1287
- const links = alepha.context.get("links")?.links ?? [];
1288
- const can = links.find((it) => it.name === name);
1417
+ const linkProvider = alepha.inject(LinkProvider);
1418
+ const can = linkProvider.getServerLinks().find((link) => link.name === name);
1289
1419
  if (can) {
1290
- 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;
1291
1421
  if (schema$1) {
1292
1422
  can.schema = schema$1;
1293
1423
  return schema$1;
@@ -1295,34 +1425,11 @@ const ssrSchemaLoading = (alepha, name) => {
1295
1425
  }
1296
1426
  return { loading: true };
1297
1427
  }
1298
- 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;
1299
1429
  if (schema) return schema;
1300
1430
  return { loading: true };
1301
1431
  };
1302
1432
 
1303
- //#endregion
1304
- //#region src/hooks/useStore.ts
1305
- /**
1306
- * Hook to access and mutate the Alepha state.
1307
- */
1308
- const useStore = (key) => {
1309
- const alepha = useAlepha();
1310
- const [state, setState] = useState(alepha.state(key));
1311
- useEffect(() => {
1312
- if (!alepha.isBrowser()) return;
1313
- return alepha.on("state:mutate", (ev) => {
1314
- if (ev.key === key) setState(ev.value);
1315
- });
1316
- }, []);
1317
- if (!alepha.isBrowser()) {
1318
- const value = alepha.context.get(key);
1319
- if (value !== null) return [value, (_) => {}];
1320
- }
1321
- return [state, (value) => {
1322
- alepha.state(key, value);
1323
- }];
1324
- };
1325
-
1326
1433
  //#endregion
1327
1434
  //#region src/index.ts
1328
1435
  /**
@@ -1340,12 +1447,12 @@ const AlephaReact = $module({
1340
1447
  descriptors: [$page],
1341
1448
  services: [
1342
1449
  ReactServerProvider,
1343
- PageDescriptorProvider,
1344
- ReactBrowserProvider
1450
+ ReactPageProvider,
1451
+ ReactRouter
1345
1452
  ],
1346
- 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)
1347
1454
  });
1348
1455
 
1349
1456
  //#endregion
1350
- 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, RedirectionError, 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 };
1351
1458
  //# sourceMappingURL=index.js.map