@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.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
  /**
@@ -41,6 +44,12 @@ const $page = (options) => {
41
44
  return (0, __alepha_core.createDescriptor)(PageDescriptor, options);
42
45
  };
43
46
  var PageDescriptor = class extends __alepha_core.Descriptor {
47
+ onInit() {
48
+ if (this.options.static) this.options.cache ??= {
49
+ provider: "memory",
50
+ ttl: [1, "week"]
51
+ };
52
+ }
44
53
  get name() {
45
54
  return this.options.name ?? this.config.propertyKey;
46
55
  }
@@ -49,7 +58,13 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
49
58
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
50
59
  */
51
60
  async render(options) {
52
- throw new __alepha_core.NotImplementedError("");
61
+ throw new Error("render method is not implemented in this environment");
62
+ }
63
+ match(url) {
64
+ return false;
65
+ }
66
+ pathname(config) {
67
+ return this.options.path || "";
53
68
  }
54
69
  };
55
70
  $page[__alepha_core.KIND] = PageDescriptor;
@@ -103,7 +118,7 @@ const ErrorViewer = ({ error, alepha }) => {
103
118
  heading: {
104
119
  fontSize: "20px",
105
120
  fontWeight: "bold",
106
- marginBottom: "4px"
121
+ marginBottom: "10px"
107
122
  },
108
123
  name: {
109
124
  fontSize: "16px",
@@ -222,28 +237,54 @@ const ErrorViewerProduction = () => {
222
237
  });
223
238
  };
224
239
 
225
- //#endregion
226
- //#region src/contexts/RouterContext.ts
227
- const RouterContext = (0, react.createContext)(void 0);
228
-
229
240
  //#endregion
230
241
  //#region src/contexts/RouterLayerContext.ts
231
242
  const RouterLayerContext = (0, react.createContext)(void 0);
232
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
+
233
259
  //#endregion
234
260
  //#region src/contexts/AlephaContext.ts
235
261
  const AlephaContext = (0, react.createContext)(void 0);
236
262
 
237
263
  //#endregion
238
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
+ */
239
277
  const useAlepha = () => {
240
278
  const alepha = (0, react.useContext)(AlephaContext);
241
- 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");
242
280
  return alepha;
243
281
  };
244
282
 
245
283
  //#endregion
246
284
  //#region src/hooks/useRouterEvents.ts
285
+ /**
286
+ * Subscribe to various router events.
287
+ */
247
288
  const useRouterEvents = (opts = {}, deps = []) => {
248
289
  const alepha = useAlepha();
249
290
  (0, react.useEffect)(() => {
@@ -316,17 +357,22 @@ var ErrorBoundary_default = ErrorBoundary;
316
357
  * ```
317
358
  */
318
359
  const NestedView = (props) => {
319
- const app = (0, react.useContext)(RouterContext);
320
360
  const layer = (0, react.useContext)(RouterLayerContext);
321
361
  const index = layer?.index ?? 0;
322
- const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
323
- useRouterEvents({ onEnd: ({ state }) => {
324
- if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
325
- } }, [app]);
326
- 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
+ } }, []);
327
369
  const element = view ?? props.children ?? null;
328
370
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
329
- fallback: app.context.onError,
371
+ fallback: (error) => {
372
+ const result = state.onError(error, state);
373
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
374
+ return result;
375
+ },
330
376
  children: element
331
377
  });
332
378
  };
@@ -334,7 +380,7 @@ var NestedView_default = NestedView;
334
380
 
335
381
  //#endregion
336
382
  //#region src/components/NotFound.tsx
337
- function NotFoundPage() {
383
+ function NotFoundPage(props) {
338
384
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
339
385
  style: {
340
386
  height: "100vh",
@@ -344,34 +390,25 @@ function NotFoundPage() {
344
390
  alignItems: "center",
345
391
  textAlign: "center",
346
392
  fontFamily: "sans-serif",
347
- padding: "1rem"
393
+ padding: "1rem",
394
+ ...props.style
348
395
  },
349
396
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
350
397
  style: {
351
398
  fontSize: "1rem",
352
399
  marginBottom: "0.5rem"
353
400
  },
354
- children: "This page does not exist"
401
+ children: "404 - This page does not exist"
355
402
  })
356
403
  });
357
404
  }
358
405
 
359
406
  //#endregion
360
- //#region src/errors/RedirectionError.ts
361
- var RedirectionError = class extends Error {
362
- page;
363
- constructor(page) {
364
- super("Redirection");
365
- this.page = page;
366
- }
367
- };
368
-
369
- //#endregion
370
- //#region src/providers/PageDescriptorProvider.ts
371
- const envSchema$1 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
372
- var PageDescriptorProvider = class {
373
- log = (0, __alepha_core.$logger)();
374
- 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);
375
412
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
376
413
  pages = [];
377
414
  getPages() {
@@ -381,7 +418,7 @@ var PageDescriptorProvider = class {
381
418
  for (const page of this.pages) if (page.name === name) return page;
382
419
  throw new Error(`Page ${name} not found`);
383
420
  }
384
- url(name, options = {}) {
421
+ pathname(name, options = {}) {
385
422
  const page = this.page(name);
386
423
  if (!page) throw new Error(`Page ${name} not found`);
387
424
  let url = page.path ?? "";
@@ -391,22 +428,28 @@ var PageDescriptorProvider = class {
391
428
  parent = parent.parent;
392
429
  }
393
430
  url = this.compile(url, options.params ?? {});
394
- return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
431
+ if (options.query) {
432
+ const query = new URLSearchParams(options.query);
433
+ if (query.toString()) url += `?${query.toString()}`;
434
+ }
435
+ return url.replace(/\/\/+/g, "/") || "/";
436
+ }
437
+ url(name, options = {}) {
438
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
395
439
  }
396
- root(state, context) {
397
- const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(RouterContext.Provider, { value: {
398
- state,
399
- context
400
- } }, (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));
401
442
  if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
402
443
  return root;
403
444
  }
404
- async createLayers(route, request) {
405
- const { pathname, search } = request.url;
406
- 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 = []) {
407
451
  let context = {};
408
452
  const stack = [{ route }];
409
- request.onError = (error) => this.renderError(error);
410
453
  let parent = route.parent;
411
454
  while (parent) {
412
455
  stack.unshift({ route: parent });
@@ -418,19 +461,18 @@ var PageDescriptorProvider = class {
418
461
  const route$1 = it.route;
419
462
  const config = {};
420
463
  try {
421
- 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) : {};
422
465
  } catch (e) {
423
466
  it.error = e;
424
467
  break;
425
468
  }
426
469
  try {
427
- 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) : {};
428
471
  } catch (e) {
429
472
  it.error = e;
430
473
  break;
431
474
  }
432
475
  it.config = { ...config };
433
- const previous = request.previous;
434
476
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
435
477
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
436
478
  const prev = JSON.stringify({
@@ -456,7 +498,7 @@ var PageDescriptorProvider = class {
456
498
  if (!route$1.resolve) continue;
457
499
  try {
458
500
  const props = await route$1.resolve?.({
459
- ...request,
501
+ ...state,
460
502
  ...config,
461
503
  ...context
462
504
  }) ?? {};
@@ -466,13 +508,8 @@ var PageDescriptorProvider = class {
466
508
  ...props
467
509
  };
468
510
  } catch (e) {
469
- if (e instanceof RedirectionError) return {
470
- layers: [],
471
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
472
- pathname,
473
- search
474
- };
475
- this.log.error(e);
511
+ if (e instanceof Redirection) return { redirect: e.redirect };
512
+ this.log.error("Page resolver has failed", e);
476
513
  it.error = e;
477
514
  break;
478
515
  }
@@ -487,44 +524,59 @@ var PageDescriptorProvider = class {
487
524
  acc += it.route.path ? this.compile(it.route.path, params) : "";
488
525
  const path = acc.replace(/\/+/, "/");
489
526
  const localErrorHandler = this.getErrorHandler(it.route);
490
- if (localErrorHandler) request.onError = localErrorHandler;
491
- if (it.error) {
492
- let element$1 = await request.onError(it.error);
493
- if (element$1 === null) element$1 = this.renderError(it.error);
494
- layers.push({
527
+ if (localErrorHandler) {
528
+ const onErrorParent = state.onError;
529
+ state.onError = (error, context$1) => {
530
+ const result = localErrorHandler(error, context$1);
531
+ if (result === void 0) return onErrorParent(error, context$1);
532
+ return result;
533
+ };
534
+ }
535
+ if (!it.error) try {
536
+ const element = await this.createElement(it.route, {
537
+ ...props,
538
+ ...context
539
+ });
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({
495
560
  props,
496
561
  error: it.error,
497
562
  name: it.route.name,
498
563
  part: it.route.path,
499
564
  config: it.config,
500
- element: this.renderView(i + 1, path, element$1, it.route),
565
+ element: this.renderView(i + 1, path, element, it.route),
501
566
  index: i + 1,
502
567
  path,
503
568
  route: it.route
504
569
  });
505
570
  break;
571
+ } catch (e) {
572
+ if (e instanceof Redirection) return { redirect: e.redirect };
573
+ throw e;
506
574
  }
507
- const element = await this.createElement(it.route, {
508
- ...props,
509
- ...context
510
- });
511
- layers.push({
512
- name: it.route.name,
513
- props,
514
- part: it.route.path,
515
- config: it.config,
516
- element: this.renderView(i + 1, path, element, it.route),
517
- index: i + 1,
518
- path,
519
- route: it.route,
520
- cache: it.cache
521
- });
522
575
  }
523
- return {
524
- layers,
525
- pathname,
526
- search
527
- };
576
+ return { state };
577
+ }
578
+ createRedirectionLayer(redirect) {
579
+ return { redirect };
528
580
  }
529
581
  getErrorHandler(route) {
530
582
  if (route.errorHandler) return route.errorHandler;
@@ -581,6 +633,7 @@ var PageDescriptorProvider = class {
581
633
  let hasNotFoundHandler = false;
582
634
  const pages = this.alepha.descriptors($page);
583
635
  const hasParent = (it) => {
636
+ if (it.options.parent) return true;
584
637
  for (const page of pages) {
585
638
  const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
586
639
  if (children.includes(it)) return true;
@@ -596,7 +649,7 @@ var PageDescriptorProvider = class {
596
649
  name: "notFound",
597
650
  cache: true,
598
651
  component: NotFoundPage,
599
- afterHandler: ({ reply }) => {
652
+ onServerResponse: ({ reply }) => {
600
653
  reply.status = 404;
601
654
  }
602
655
  });
@@ -604,6 +657,12 @@ var PageDescriptorProvider = class {
604
657
  });
605
658
  map(pages, target) {
606
659
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
660
+ const getChildrenFromParent = (it) => {
661
+ const children$1 = [];
662
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
663
+ return children$1;
664
+ };
665
+ children.push(...getChildrenFromParent(target));
607
666
  return {
608
667
  ...target.options,
609
668
  name: target.name,
@@ -643,212 +702,9 @@ const isPageRoute = (it) => {
643
702
  return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
644
703
  };
645
704
 
646
- //#endregion
647
- //#region src/providers/BrowserRouterProvider.ts
648
- var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
649
- log = (0, __alepha_core.$logger)();
650
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
651
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
652
- add(entry) {
653
- this.pageDescriptorProvider.add(entry);
654
- }
655
- configure = (0, __alepha_core.$hook)({
656
- on: "configure",
657
- handler: async () => {
658
- for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
659
- path: page.match,
660
- page
661
- });
662
- }
663
- });
664
- async transition(url, options = {}) {
665
- const { pathname, search } = url;
666
- const state = {
667
- pathname,
668
- search,
669
- layers: []
670
- };
671
- const context = {
672
- url,
673
- query: {},
674
- params: {},
675
- onError: () => null,
676
- ...options.context ?? {}
677
- };
678
- await this.alepha.emit("react:transition:begin", {
679
- state,
680
- context
681
- });
682
- try {
683
- const previous = options.previous;
684
- const { route, params } = this.match(pathname);
685
- const query = {};
686
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
687
- context.query = query;
688
- context.params = params ?? {};
689
- context.previous = previous;
690
- if (isPageRoute(route)) {
691
- const result = await this.pageDescriptorProvider.createLayers(route.page, context);
692
- if (result.redirect) return {
693
- redirect: result.redirect,
694
- state,
695
- context
696
- };
697
- state.layers = result.layers;
698
- }
699
- if (state.layers.length === 0) state.layers.push({
700
- name: "not-found",
701
- element: (0, react.createElement)(NotFoundPage),
702
- index: 0,
703
- path: "/"
704
- });
705
- await this.alepha.emit("react:transition:success", {
706
- state,
707
- context
708
- });
709
- } catch (e) {
710
- this.log.error(e);
711
- state.layers = [{
712
- name: "error",
713
- element: this.pageDescriptorProvider.renderError(e),
714
- index: 0,
715
- path: "/"
716
- }];
717
- await this.alepha.emit("react:transition:error", {
718
- error: e,
719
- state,
720
- context
721
- });
722
- }
723
- if (options.state) {
724
- options.state.layers = state.layers;
725
- options.state.pathname = state.pathname;
726
- options.state.search = state.search;
727
- }
728
- await this.alepha.emit("react:transition:end", {
729
- state: options.state,
730
- context
731
- });
732
- return {
733
- context,
734
- state
735
- };
736
- }
737
- root(state, context) {
738
- return this.pageDescriptorProvider.root(state, context);
739
- }
740
- };
741
-
742
- //#endregion
743
- //#region src/providers/ReactBrowserProvider.ts
744
- var ReactBrowserProvider = class {
745
- log = (0, __alepha_core.$logger)();
746
- client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
747
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
748
- router = (0, __alepha_core.$inject)(BrowserRouterProvider);
749
- root;
750
- transitioning;
751
- state = {
752
- layers: [],
753
- pathname: "",
754
- search: ""
755
- };
756
- get document() {
757
- return window.document;
758
- }
759
- get history() {
760
- return window.history;
761
- }
762
- get location() {
763
- return window.location;
764
- }
765
- get url() {
766
- let url = this.location.pathname + this.location.search;
767
- return url;
768
- }
769
- pushState(url, replace) {
770
- let path = url;
771
- if (replace) this.history.replaceState({}, "", path);
772
- else this.history.pushState({}, "", path);
773
- }
774
- async invalidate(props) {
775
- const previous = [];
776
- if (props) {
777
- const [key] = Object.keys(props);
778
- const value = props[key];
779
- for (const layer of this.state.layers) {
780
- if (layer.props?.[key]) {
781
- previous.push({
782
- ...layer,
783
- props: {
784
- ...layer.props,
785
- [key]: value
786
- }
787
- });
788
- break;
789
- }
790
- previous.push(layer);
791
- }
792
- }
793
- await this.render({ previous });
794
- }
795
- async go(url, options = {}) {
796
- const result = await this.render({ url });
797
- if (result.context.url.pathname !== url) {
798
- this.pushState(result.context.url.pathname);
799
- return;
800
- }
801
- if (options.replace) {
802
- this.pushState(url);
803
- return;
804
- }
805
- this.pushState(url);
806
- }
807
- async render(options = {}) {
808
- const previous = options.previous ?? this.state.layers;
809
- const url = options.url ?? this.url;
810
- this.transitioning = { to: url };
811
- const result = await this.router.transition(new URL(`http://localhost${url}`), {
812
- previous,
813
- state: this.state
814
- });
815
- if (result.redirect) return await this.render({ url: result.redirect });
816
- this.transitioning = void 0;
817
- return result;
818
- }
819
- /**
820
- * Get embedded layers from the server.
821
- */
822
- getHydrationState() {
823
- try {
824
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
825
- } catch (error) {
826
- console.error(error);
827
- }
828
- }
829
- ready = (0, __alepha_core.$hook)({
830
- on: "ready",
831
- handler: async () => {
832
- const hydration = this.getHydrationState();
833
- const previous = hydration?.layers ?? [];
834
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
835
- const { context } = await this.render({ previous });
836
- await this.alepha.emit("react:browser:render", {
837
- state: this.state,
838
- context,
839
- hydration
840
- });
841
- window.addEventListener("popstate", () => {
842
- if (this.state.pathname === this.url) return;
843
- this.render();
844
- });
845
- }
846
- });
847
- };
848
-
849
705
  //#endregion
850
706
  //#region src/providers/ReactServerProvider.ts
851
- const envSchema = __alepha_core.t.object({
707
+ const envSchema$1 = __alepha_core.t.object({
852
708
  REACT_SERVER_DIST: __alepha_core.t.string({ default: "public" }),
853
709
  REACT_SERVER_PREFIX: __alepha_core.t.string({ default: "" }),
854
710
  REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
@@ -856,13 +712,13 @@ const envSchema = __alepha_core.t.object({
856
712
  REACT_SERVER_TEMPLATE: __alepha_core.t.optional(__alepha_core.t.string({ size: "rich" }))
857
713
  });
858
714
  var ReactServerProvider = class {
859
- log = (0, __alepha_core.$logger)();
715
+ log = (0, __alepha_logger.$logger)();
860
716
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
861
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
717
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
862
718
  serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
863
719
  serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
864
720
  serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
865
- env = (0, __alepha_core.$env)(envSchema);
721
+ env = (0, __alepha_core.$env)(envSchema$1);
866
722
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
867
723
  onConfigure = (0, __alepha_core.$hook)({
868
724
  on: "configure",
@@ -909,7 +765,7 @@ var ReactServerProvider = class {
909
765
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
910
766
  }
911
767
  async registerPages(templateLoader) {
912
- for (const page of this.pageDescriptorProvider.getPages()) {
768
+ for (const page of this.pageApi.getPages()) {
913
769
  if (page.children?.length) continue;
914
770
  this.log.debug(`+ ${page.match} -> ${page.name}`);
915
771
  this.serverRouterProvider.createRoute({
@@ -943,24 +799,30 @@ var ReactServerProvider = class {
943
799
  */
944
800
  createRenderFunction(name, withIndex = false) {
945
801
  return async (options = {}) => {
946
- const page = this.pageDescriptorProvider.page(name);
947
- const url = new URL(this.pageDescriptorProvider.url(name, options));
948
- const context = {
802
+ const page = this.pageApi.page(name);
803
+ const url = new URL(this.pageApi.url(name, options));
804
+ const entry = {
949
805
  url,
950
806
  params: options.params ?? {},
951
807
  query: options.query ?? {},
952
- head: {},
953
- onError: () => null
954
- };
955
- await this.alepha.emit("react:server:render:begin", { context });
956
- const state = await this.pageDescriptorProvider.createLayers(page, context);
957
- if (!withIndex && !options.html) return {
958
- context,
959
- html: (0, react_dom_server.renderToString)(this.pageDescriptorProvider.root(state, context))
808
+ onError: () => null,
809
+ layers: []
960
810
  };
961
- 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);
824
+ if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
962
825
  const result = {
963
- context,
964
826
  state,
965
827
  html
966
828
  };
@@ -968,30 +830,27 @@ var ReactServerProvider = class {
968
830
  return result;
969
831
  };
970
832
  }
971
- createHandler(page, templateLoader) {
833
+ createHandler(route, templateLoader) {
972
834
  return async (serverRequest) => {
973
835
  const { url, reply, query, params } = serverRequest;
974
836
  const template = await templateLoader();
975
837
  if (!template) throw new Error("Template not found");
976
- const context = {
838
+ this.log.trace("Rendering page", { name: route.name });
839
+ const entry = {
977
840
  url,
978
841
  params,
979
842
  query,
980
- head: {},
981
- onError: () => null
843
+ onError: () => null,
844
+ layers: []
982
845
  };
983
- if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) {
984
- const srv = this.alepha.inject(__alepha_server_links.ServerLinksProvider);
985
- const schema = __alepha_server.apiLinksResponseSchema;
986
- context.links = this.alepha.parse(schema, await srv.getLinks({
987
- user: serverRequest.user,
988
- authorization: serverRequest.headers.authorization
989
- }));
990
- this.alepha.context.set("links", context.links);
991
- }
992
- 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;
993
852
  while (target) {
994
- if (page.can && !page.can()) {
853
+ if (route.can && !route.can()) {
995
854
  reply.status = 403;
996
855
  reply.headers["content-type"] = "text/plain";
997
856
  return "Forbidden";
@@ -1000,50 +859,60 @@ var ReactServerProvider = class {
1000
859
  }
1001
860
  await this.alepha.emit("react:server:render:begin", {
1002
861
  request: serverRequest,
1003
- context
862
+ state
1004
863
  });
1005
864
  this.serverTimingProvider.beginTiming("createLayers");
1006
- const state = await this.pageDescriptorProvider.createLayers(page, context);
865
+ const { redirect } = await this.pageApi.createLayers(route, state);
1007
866
  this.serverTimingProvider.endTiming("createLayers");
1008
- if (state.redirect) return reply.redirect(state.redirect);
867
+ if (redirect) return reply.redirect(redirect);
1009
868
  reply.headers["content-type"] = "text/html";
1010
869
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
1011
870
  reply.headers.pragma = "no-cache";
1012
871
  reply.headers.expires = "0";
1013
- if (page.cache && serverRequest.user) delete context.links;
1014
- const html = this.renderToHtml(template, state, context);
1015
- await this.alepha.emit("react:server:render:end", {
872
+ const html = this.renderToHtml(template, state);
873
+ if (html instanceof Redirection) {
874
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
875
+ return;
876
+ }
877
+ const event = {
1016
878
  request: serverRequest,
1017
- context,
1018
879
  state,
1019
880
  html
1020
- });
1021
- page.afterHandler?.(serverRequest);
1022
- return html;
881
+ };
882
+ await this.alepha.emit("react:server:render:end", event);
883
+ route.onServerResponse?.(serverRequest);
884
+ this.log.trace("Page rendered", { name: route.name });
885
+ return event.html;
1023
886
  };
1024
887
  }
1025
- renderToHtml(template, state, context, hydration = true) {
1026
- 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);
1027
891
  this.serverTimingProvider.beginTiming("renderToString");
1028
892
  let app = "";
1029
893
  try {
1030
894
  app = (0, react_dom_server.renderToString)(element);
1031
895
  } catch (error) {
1032
- this.log.error("Error during SSR", error);
1033
- app = (0, react_dom_server.renderToString)(context.onError(error));
896
+ this.log.error("renderToString has failed, fallback to error handler", error);
897
+ const element$1 = state.onError(error, state);
898
+ if (element$1 instanceof Redirection) return element$1;
899
+ app = (0, react_dom_server.renderToString)(element$1);
900
+ this.log.debug("Error handled successfully with fallback");
1034
901
  }
1035
902
  this.serverTimingProvider.endTiming("renderToString");
1036
903
  const response = { html: template };
1037
904
  if (hydration) {
905
+ const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
1038
906
  const hydrationData = {
1039
- links: context.links,
907
+ ...store,
908
+ "react.router.state": void 0,
1040
909
  layers: state.layers.map((it) => ({
1041
910
  ...it,
1042
911
  error: it.error ? {
1043
912
  ...it.error,
1044
913
  name: it.error.name,
1045
914
  message: it.error.message,
1046
- stack: it.error.stack
915
+ stack: !this.alepha.isProduction() ? it.error.stack : void 0
1047
916
  } : void 0,
1048
917
  index: void 0,
1049
918
  path: void 0,
@@ -1051,7 +920,7 @@ var ReactServerProvider = class {
1051
920
  route: void 0
1052
921
  }))
1053
922
  };
1054
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
923
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1055
924
  this.fillTemplate(response, app, script);
1056
925
  }
1057
926
  return response.html;
@@ -1063,26 +932,269 @@ var ReactServerProvider = class {
1063
932
  else {
1064
933
  const bodyOpenTag = /<body([^>]*)>/i;
1065
934
  if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1066
- return `${match}\n<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
935
+ return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1067
936
  });
1068
937
  }
1069
938
  const bodyCloseTagRegex = /<\/body>/i;
1070
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}\n</body>`);
939
+ if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
1071
940
  }
1072
941
  };
1073
942
 
1074
943
  //#endregion
1075
- //#region src/hooks/RouterHookApi.ts
1076
- var RouterHookApi = class {
1077
- constructor(pages, context, state, layer, browser) {
1078
- this.pages = pages;
1079
- this.context = context;
1080
- this.state = state;
1081
- this.layer = layer;
1082
- 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;
1186
+ }
1187
+ path(name, config = {}) {
1188
+ return this.pageApi.pathname(name, {
1189
+ params: {
1190
+ ...this.state.params,
1191
+ ...config.params
1192
+ },
1193
+ query: config.query
1194
+ });
1083
1195
  }
1084
1196
  getURL() {
1085
- if (!this.browser) return this.context.url;
1197
+ if (!this.browser) return this.state.url;
1086
1198
  return new URL(this.location.href);
1087
1199
  }
1088
1200
  get location() {
@@ -1093,11 +1205,11 @@ var RouterHookApi = class {
1093
1205
  return this.state;
1094
1206
  }
1095
1207
  get pathname() {
1096
- return this.state.pathname;
1208
+ return this.state.url.pathname;
1097
1209
  }
1098
1210
  get query() {
1099
1211
  const query = {};
1100
- 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);
1101
1213
  return query;
1102
1214
  }
1103
1215
  async back() {
@@ -1109,39 +1221,33 @@ var RouterHookApi = class {
1109
1221
  async invalidate(props) {
1110
1222
  await this.browser?.invalidate(props);
1111
1223
  }
1112
- /**
1113
- * Create a valid href for the given pathname.
1114
- *
1115
- * @param pathname
1116
- * @param layer
1117
- */
1118
- createHref(pathname, layer = this.layer, options = {}) {
1119
- if (typeof pathname === "object") pathname = pathname.options.path ?? "";
1120
- if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
1121
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
1122
- }
1123
1224
  async go(path, options) {
1124
1225
  for (const page of this.pages) if (page.name === path) {
1125
- path = page.path ?? "";
1126
- break;
1226
+ await this.browser?.go(this.path(path, options), options);
1227
+ return;
1127
1228
  }
1128
- await this.browser?.go(this.createHref(path, this.layer, options), options);
1229
+ await this.browser?.go(path, options);
1129
1230
  }
1130
1231
  anchor(path, options = {}) {
1232
+ let href = path;
1131
1233
  for (const page of this.pages) if (page.name === path) {
1132
- path = page.path ?? "";
1234
+ href = this.path(path, options);
1133
1235
  break;
1134
1236
  }
1135
- const href = this.createHref(path, this.layer, options);
1136
1237
  return {
1137
- href,
1238
+ href: this.base(href),
1138
1239
  onClick: (ev) => {
1139
1240
  ev.stopPropagation();
1140
1241
  ev.preventDefault();
1141
- this.go(path, options).catch(console.error);
1242
+ this.go(href, options).catch(console.error);
1142
1243
  }
1143
1244
  };
1144
1245
  }
1246
+ base(path) {
1247
+ const base = {}.env?.BASE_URL;
1248
+ if (!base || base === "/") return path;
1249
+ return base + path;
1250
+ }
1145
1251
  /**
1146
1252
  * Set query params.
1147
1253
  *
@@ -1157,90 +1263,131 @@ var RouterHookApi = class {
1157
1263
  }
1158
1264
  };
1159
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
+
1160
1277
  //#endregion
1161
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
+ */
1162
1293
  const useRouter = () => {
1163
- const alepha = useAlepha();
1164
- const ctx = (0, react.useContext)(RouterContext);
1165
- const layer = (0, react.useContext)(RouterLayerContext);
1166
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1167
- const pages = (0, react.useMemo)(() => {
1168
- return alepha.inject(PageDescriptorProvider).getPages();
1169
- }, []);
1170
- return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1294
+ return useInject(ReactRouter);
1171
1295
  };
1172
1296
 
1173
1297
  //#endregion
1174
1298
  //#region src/components/Link.tsx
1175
1299
  const Link = (props) => {
1176
- react.default.useContext(RouterContext);
1177
1300
  const router = useRouter();
1178
- const to = typeof props.to === "string" ? props.to : props.to.options.path;
1179
- if (!to) return null;
1180
- const can = typeof props.to === "string" ? void 0 : props.to.options.can;
1181
- if (can && !can()) return null;
1182
- const name = typeof props.to === "string" ? void 0 : props.to.options.name;
1183
- const anchorProps = {
1184
- ...props,
1185
- to: void 0
1186
- };
1301
+ const { to,...anchorProps } = props;
1187
1302
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1188
1303
  ...router.anchor(to),
1189
1304
  ...anchorProps,
1190
- children: props.children ?? name
1305
+ children: props.children
1191
1306
  });
1192
1307
  };
1193
1308
  var Link_default = Link;
1194
1309
 
1310
+ //#endregion
1311
+ //#region src/hooks/useStore.ts
1312
+ /**
1313
+ * Hook to access and mutate the Alepha state.
1314
+ */
1315
+ const useStore = (key, defaultValue) => {
1316
+ const alepha = useAlepha();
1317
+ (0, react.useMemo)(() => {
1318
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1319
+ }, [defaultValue]);
1320
+ const [state, setState] = (0, react.useState)(alepha.state(key));
1321
+ (0, react.useEffect)(() => {
1322
+ if (!alepha.isBrowser()) return;
1323
+ return alepha.on("state:mutate", (ev) => {
1324
+ if (ev.key === key) setState(ev.value);
1325
+ });
1326
+ }, []);
1327
+ return [state, (value) => {
1328
+ alepha.state(key, value);
1329
+ }];
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
+
1195
1340
  //#endregion
1196
1341
  //#region src/hooks/useActive.ts
1197
- const useActive = (path) => {
1342
+ const useActive = (args) => {
1198
1343
  const router = useRouter();
1199
- const ctx = (0, react.useContext)(RouterContext);
1200
- const layer = (0, react.useContext)(RouterLayerContext);
1201
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1202
- let name;
1203
- if (typeof path === "object" && path.options.name) name = path.options.name;
1204
- const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
1205
- const href = (0, react.useMemo)(() => router.createHref(path, layer), [path, layer]);
1206
1344
  const [isPending, setPending] = (0, react.useState)(false);
1207
- const isActive = current === href;
1208
- useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
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);
1209
1354
  return {
1210
- name,
1211
1355
  isPending,
1212
1356
  isActive,
1213
1357
  anchorProps: {
1214
- href,
1215
- onClick: (ev) => {
1216
- ev.stopPropagation();
1217
- ev.preventDefault();
1358
+ href: router.base(href),
1359
+ onClick: async (ev) => {
1360
+ ev?.stopPropagation();
1361
+ ev?.preventDefault();
1218
1362
  if (isActive) return;
1219
1363
  if (isPending) return;
1220
1364
  setPending(true);
1221
- router.go(href).then(() => {
1365
+ try {
1366
+ await router.go(href);
1367
+ } finally {
1222
1368
  setPending(false);
1223
- });
1369
+ }
1224
1370
  }
1225
1371
  }
1226
1372
  };
1227
1373
  };
1228
1374
 
1229
- //#endregion
1230
- //#region src/hooks/useInject.ts
1231
- const useInject = (service) => {
1232
- const alepha = useAlepha();
1233
- return (0, react.useMemo)(() => alepha.inject(service), []);
1234
- };
1235
-
1236
1375
  //#endregion
1237
1376
  //#region src/hooks/useClient.ts
1238
- const useClient = (_scope) => {
1239
- 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);
1240
1384
  };
1241
1385
 
1242
1386
  //#endregion
1243
1387
  //#region src/hooks/useQueryParams.ts
1388
+ /**
1389
+ * Not well tested. Use with caution.
1390
+ */
1244
1391
  const useQueryParams = (schema, options = {}) => {
1245
1392
  const alepha = useAlepha();
1246
1393
  const key = options.key ?? "q";
@@ -1271,29 +1418,17 @@ const decode = (alepha, schema, data) => {
1271
1418
  }
1272
1419
  };
1273
1420
 
1274
- //#endregion
1275
- //#region src/hooks/useRouterState.ts
1276
- const useRouterState = () => {
1277
- const router = (0, react.useContext)(RouterContext);
1278
- const layer = (0, react.useContext)(RouterLayerContext);
1279
- if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1280
- const [state, setState] = (0, react.useState)(router.state);
1281
- useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1282
- return state;
1283
- };
1284
-
1285
1421
  //#endregion
1286
1422
  //#region src/hooks/useSchema.ts
1287
1423
  const useSchema = (action) => {
1288
1424
  const name = action.name;
1289
1425
  const alepha = useAlepha();
1290
1426
  const httpClient = useInject(__alepha_server.HttpClient);
1291
- const linkProvider = useInject(__alepha_server_links.LinkProvider);
1292
1427
  const [schema, setSchema] = (0, react.useState)(ssrSchemaLoading(alepha, name));
1293
1428
  (0, react.useEffect)(() => {
1294
1429
  if (!schema.loading) return;
1295
1430
  const opts = { cache: true };
1296
- 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));
1297
1432
  }, [name]);
1298
1433
  return schema;
1299
1434
  };
@@ -1302,10 +1437,10 @@ const useSchema = (action) => {
1302
1437
  */
1303
1438
  const ssrSchemaLoading = (alepha, name) => {
1304
1439
  if (!alepha.isBrowser()) {
1305
- const links = alepha.context.get("links")?.links ?? [];
1306
- 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);
1307
1442
  if (can) {
1308
- 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;
1309
1444
  if (schema$1) {
1310
1445
  can.schema = schema$1;
1311
1446
  return schema$1;
@@ -1313,34 +1448,11 @@ const ssrSchemaLoading = (alepha, name) => {
1313
1448
  }
1314
1449
  return { loading: true };
1315
1450
  }
1316
- 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;
1317
1452
  if (schema) return schema;
1318
1453
  return { loading: true };
1319
1454
  };
1320
1455
 
1321
- //#endregion
1322
- //#region src/hooks/useStore.ts
1323
- /**
1324
- * Hook to access and mutate the Alepha state.
1325
- */
1326
- const useStore = (key) => {
1327
- const alepha = useAlepha();
1328
- const [state, setState] = (0, react.useState)(alepha.state(key));
1329
- (0, react.useEffect)(() => {
1330
- if (!alepha.isBrowser()) return;
1331
- return alepha.on("state:mutate", (ev) => {
1332
- if (ev.key === key) setState(ev.value);
1333
- });
1334
- }, []);
1335
- if (!alepha.isBrowser()) {
1336
- const value = alepha.context.get(key);
1337
- if (value !== null) return [value, (_) => {}];
1338
- }
1339
- return [state, (value) => {
1340
- alepha.state(key, value);
1341
- }];
1342
- };
1343
-
1344
1456
  //#endregion
1345
1457
  //#region src/index.ts
1346
1458
  /**
@@ -1358,10 +1470,10 @@ const AlephaReact = (0, __alepha_core.$module)({
1358
1470
  descriptors: [$page],
1359
1471
  services: [
1360
1472
  ReactServerProvider,
1361
- PageDescriptorProvider,
1362
- ReactBrowserProvider
1473
+ ReactPageProvider,
1474
+ ReactRouter
1363
1475
  ],
1364
- 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)
1365
1477
  });
1366
1478
 
1367
1479
  //#endregion
@@ -1370,16 +1482,16 @@ exports.AlephaContext = AlephaContext;
1370
1482
  exports.AlephaReact = AlephaReact;
1371
1483
  exports.ClientOnly = ClientOnly_default;
1372
1484
  exports.ErrorBoundary = ErrorBoundary_default;
1485
+ exports.ErrorViewer = ErrorViewer_default;
1373
1486
  exports.Link = Link_default;
1374
1487
  exports.NestedView = NestedView_default;
1375
1488
  exports.NotFound = NotFoundPage;
1376
1489
  exports.PageDescriptor = PageDescriptor;
1377
- exports.PageDescriptorProvider = PageDescriptorProvider;
1378
1490
  exports.ReactBrowserProvider = ReactBrowserProvider;
1491
+ exports.ReactPageProvider = ReactPageProvider;
1492
+ exports.ReactRouter = ReactRouter;
1379
1493
  exports.ReactServerProvider = ReactServerProvider;
1380
- exports.RedirectionError = RedirectionError;
1381
- exports.RouterContext = RouterContext;
1382
- exports.RouterHookApi = RouterHookApi;
1494
+ exports.Redirection = Redirection;
1383
1495
  exports.RouterLayerContext = RouterLayerContext;
1384
1496
  exports.isPageRoute = isPageRoute;
1385
1497
  exports.ssrSchemaLoading = ssrSchemaLoading;