@alepha/react 0.9.3 → 0.9.5

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 (39) hide show
  1. package/README.md +64 -6
  2. package/dist/index.browser.js +442 -328
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +644 -482
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +402 -339
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +412 -349
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +641 -484
  11. package/dist/index.js.map +1 -1
  12. package/package.json +16 -11
  13. package/src/components/Link.tsx +2 -5
  14. package/src/components/NestedView.tsx +164 -19
  15. package/src/components/NotFound.tsx +1 -1
  16. package/src/descriptors/$page.ts +100 -5
  17. package/src/errors/Redirection.ts +8 -5
  18. package/src/hooks/useActive.ts +25 -35
  19. package/src/hooks/useAlepha.ts +16 -2
  20. package/src/hooks/useClient.ts +7 -4
  21. package/src/hooks/useInject.ts +4 -1
  22. package/src/hooks/useQueryParams.ts +9 -6
  23. package/src/hooks/useRouter.ts +18 -31
  24. package/src/hooks/useRouterEvents.ts +30 -22
  25. package/src/hooks/useRouterState.ts +8 -20
  26. package/src/hooks/useSchema.ts +10 -15
  27. package/src/hooks/useStore.ts +0 -7
  28. package/src/index.browser.ts +14 -11
  29. package/src/index.shared.ts +2 -3
  30. package/src/index.ts +27 -31
  31. package/src/providers/ReactBrowserProvider.ts +151 -62
  32. package/src/providers/ReactBrowserRendererProvider.ts +22 -0
  33. package/src/providers/ReactBrowserRouterProvider.ts +137 -0
  34. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +121 -104
  35. package/src/providers/ReactServerProvider.ts +90 -76
  36. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +49 -62
  37. package/src/contexts/RouterContext.ts +0 -14
  38. package/src/providers/BrowserRouterProvider.ts +0 -155
  39. package/src/providers/ReactBrowserRenderer.ts +0 -93
package/dist/index.cjs CHANGED
@@ -25,13 +25,15 @@ 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 __alepha_router = __toESM(require("@alepha/router"));
35
37
 
36
38
  //#region src/descriptors/$page.ts
37
39
  /**
@@ -55,7 +57,16 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
55
57
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
56
58
  */
57
59
  async render(options) {
58
- throw new Error("render method is not implemented in this environment");
60
+ throw new __alepha_core.AlephaError("render() method is not implemented in this environment");
61
+ }
62
+ async fetch(options) {
63
+ throw new __alepha_core.AlephaError("fetch() method is not implemented in this environment");
64
+ }
65
+ match(url) {
66
+ return false;
67
+ }
68
+ pathname(config) {
69
+ return this.options.path || "";
59
70
  }
60
71
  };
61
72
  $page[__alepha_core.KIND] = PageDescriptor;
@@ -78,7 +89,6 @@ const ClientOnly = (props) => {
78
89
  if (props.disabled) return props.children;
79
90
  return mounted ? props.children : props.fallback;
80
91
  };
81
- var ClientOnly_default = ClientOnly;
82
92
 
83
93
  //#endregion
84
94
  //#region src/components/ErrorViewer.tsx
@@ -186,7 +196,6 @@ const ErrorViewer = ({ error, alepha }) => {
186
196
  })] })]
187
197
  });
188
198
  };
189
- var ErrorViewer_default = ErrorViewer;
190
199
  const ErrorViewerProduction = () => {
191
200
  const styles = {
192
201
  container: {
@@ -228,45 +237,107 @@ const ErrorViewerProduction = () => {
228
237
  });
229
238
  };
230
239
 
231
- //#endregion
232
- //#region src/contexts/RouterContext.ts
233
- const RouterContext = (0, react.createContext)(void 0);
234
-
235
240
  //#endregion
236
241
  //#region src/contexts/RouterLayerContext.ts
237
242
  const RouterLayerContext = (0, react.createContext)(void 0);
238
243
 
244
+ //#endregion
245
+ //#region src/errors/Redirection.ts
246
+ /**
247
+ * Used for Redirection during the page loading.
248
+ *
249
+ * Depends on the context, it can be thrown or just returned.
250
+ */
251
+ var Redirection = class extends Error {
252
+ redirect;
253
+ constructor(redirect) {
254
+ super("Redirection");
255
+ this.redirect = redirect;
256
+ }
257
+ };
258
+
239
259
  //#endregion
240
260
  //#region src/contexts/AlephaContext.ts
241
261
  const AlephaContext = (0, react.createContext)(void 0);
242
262
 
243
263
  //#endregion
244
264
  //#region src/hooks/useAlepha.ts
265
+ /**
266
+ * Main Alepha hook.
267
+ *
268
+ * It provides access to the Alepha instance within a React component.
269
+ *
270
+ * With Alepha, you can access the core functionalities of the framework:
271
+ *
272
+ * - alepha.state() for state management
273
+ * - alepha.inject() for dependency injection
274
+ * - alepha.emit() for event handling
275
+ * etc...
276
+ */
245
277
  const useAlepha = () => {
246
278
  const alepha = (0, react.useContext)(AlephaContext);
247
- if (!alepha) throw new Error("useAlepha must be used within an AlephaContext.Provider");
279
+ if (!alepha) throw new __alepha_core.AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
248
280
  return alepha;
249
281
  };
250
282
 
251
283
  //#endregion
252
284
  //#region src/hooks/useRouterEvents.ts
285
+ /**
286
+ * Subscribe to various router events.
287
+ */
253
288
  const useRouterEvents = (opts = {}, deps = []) => {
254
289
  const alepha = useAlepha();
255
290
  (0, react.useEffect)(() => {
256
291
  if (!alepha.isBrowser()) return;
292
+ const cb = (callback) => {
293
+ if (typeof callback === "function") return { callback };
294
+ return callback;
295
+ };
257
296
  const subs = [];
258
297
  const onBegin = opts.onBegin;
259
298
  const onEnd = opts.onEnd;
260
299
  const onError = opts.onError;
261
- if (onBegin) subs.push(alepha.on("react:transition:begin", { callback: onBegin }));
262
- if (onEnd) subs.push(alepha.on("react:transition:end", { callback: onEnd }));
263
- if (onError) subs.push(alepha.on("react:transition:error", { callback: onError }));
300
+ const onSuccess = opts.onSuccess;
301
+ if (onBegin) subs.push(alepha.on("react:transition:begin", cb(onBegin)));
302
+ if (onEnd) subs.push(alepha.on("react:transition:end", cb(onEnd)));
303
+ if (onError) subs.push(alepha.on("react:transition:error", cb(onError)));
304
+ if (onSuccess) subs.push(alepha.on("react:transition:success", cb(onSuccess)));
264
305
  return () => {
265
306
  for (const sub of subs) sub();
266
307
  };
267
308
  }, deps);
268
309
  };
269
310
 
311
+ //#endregion
312
+ //#region src/hooks/useStore.ts
313
+ /**
314
+ * Hook to access and mutate the Alepha state.
315
+ */
316
+ const useStore = (key, defaultValue) => {
317
+ const alepha = useAlepha();
318
+ (0, react.useMemo)(() => {
319
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
320
+ }, [defaultValue]);
321
+ const [state, setState] = (0, react.useState)(alepha.state(key));
322
+ (0, react.useEffect)(() => {
323
+ if (!alepha.isBrowser()) return;
324
+ return alepha.on("state:mutate", (ev) => {
325
+ if (ev.key === key) setState(ev.value);
326
+ });
327
+ }, []);
328
+ return [state, (value) => {
329
+ alepha.state(key, value);
330
+ }];
331
+ };
332
+
333
+ //#endregion
334
+ //#region src/hooks/useRouterState.ts
335
+ const useRouterState = () => {
336
+ const [state] = useStore("react.router.state");
337
+ if (!state) throw new __alepha_core.AlephaError("Missing react router state");
338
+ return state;
339
+ };
340
+
270
341
  //#endregion
271
342
  //#region src/components/ErrorBoundary.tsx
272
343
  /**
@@ -296,7 +367,6 @@ var ErrorBoundary = class extends react.default.Component {
296
367
  return this.props.children;
297
368
  }
298
369
  };
299
- var ErrorBoundary_default = ErrorBoundary;
300
370
 
301
371
  //#endregion
302
372
  //#region src/components/NestedView.tsx
@@ -307,7 +377,7 @@ var ErrorBoundary_default = ErrorBoundary;
307
377
  *
308
378
  * @example
309
379
  * ```tsx
310
- * import { NestedView } from "@alepha/react";
380
+ * import { NestedView } from "alepha/react";
311
381
  *
312
382
  * class App {
313
383
  * parent = $page({
@@ -322,24 +392,108 @@ var ErrorBoundary_default = ErrorBoundary;
322
392
  * ```
323
393
  */
324
394
  const NestedView = (props) => {
325
- const app = (0, react.useContext)(RouterContext);
326
- const layer = (0, react.useContext)(RouterLayerContext);
327
- const index = layer?.index ?? 0;
328
- const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
329
- useRouterEvents({ onEnd: ({ state, context }) => {
330
- if (app) app.context = context;
331
- if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
332
- } }, [app]);
333
- if (!app) throw new Error("NestedView must be used within a RouterContext.");
334
- const element = view ?? props.children ?? null;
335
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
395
+ const index = (0, react.use)(RouterLayerContext)?.index ?? 0;
396
+ const state = useRouterState();
397
+ const [view, setView] = (0, react.useState)(state.layers[index]?.element);
398
+ const [animation, setAnimation] = (0, react.useState)("");
399
+ const animationExitDuration = (0, react.useRef)(0);
400
+ const animationExitNow = (0, react.useRef)(0);
401
+ useRouterEvents({
402
+ onBegin: async ({ previous, state: state$1 }) => {
403
+ const layer = previous.layers[index];
404
+ if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
405
+ const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
406
+ if (animationExit) {
407
+ const duration = animationExit.duration || 200;
408
+ animationExitNow.current = Date.now();
409
+ animationExitDuration.current = duration;
410
+ setAnimation(animationExit.animation);
411
+ } else {
412
+ animationExitNow.current = 0;
413
+ animationExitDuration.current = 0;
414
+ setAnimation("");
415
+ }
416
+ },
417
+ onEnd: async ({ state: state$1 }) => {
418
+ const layer = state$1.layers[index];
419
+ if (animationExitNow.current) {
420
+ const duration = animationExitDuration.current;
421
+ const diff = Date.now() - animationExitNow.current;
422
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
423
+ }
424
+ if (!layer?.cache) {
425
+ setView(layer?.element);
426
+ const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
427
+ if (animationEnter) setAnimation(animationEnter.animation);
428
+ else setAnimation("");
429
+ }
430
+ }
431
+ }, []);
432
+ let element = view ?? props.children ?? null;
433
+ if (animation) element = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
434
+ style: {
435
+ display: "flex",
436
+ flex: 1,
437
+ height: "100%",
438
+ width: "100%",
439
+ position: "relative",
440
+ overflow: "hidden"
441
+ },
442
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
443
+ style: {
444
+ height: "100%",
445
+ width: "100%",
446
+ display: "flex",
447
+ animation
448
+ },
449
+ children: element
450
+ })
451
+ });
452
+ if (props.errorBoundary === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: element });
453
+ if (props.errorBoundary) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
454
+ fallback: props.errorBoundary,
455
+ children: element
456
+ });
457
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
336
458
  fallback: (error) => {
337
- return app.context.onError?.(error, app.context);
459
+ const result = state.onError(error, state);
460
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
461
+ return result;
338
462
  },
339
463
  children: element
340
464
  });
341
465
  };
342
- var NestedView_default = NestedView;
466
+ var NestedView_default = (0, react.memo)(NestedView);
467
+ function parseAnimation(animationLike, state, type = "enter") {
468
+ if (!animationLike) return void 0;
469
+ const DEFAULT_DURATION = 300;
470
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
471
+ if (typeof animation === "string") {
472
+ if (type === "exit") return;
473
+ return {
474
+ duration: DEFAULT_DURATION,
475
+ animation: `${DEFAULT_DURATION}ms ${animation}`
476
+ };
477
+ }
478
+ if (typeof animation === "object") {
479
+ const anim = animation[type];
480
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
481
+ const name = typeof anim === "object" ? anim.name : anim;
482
+ if (type === "exit") {
483
+ const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
484
+ return {
485
+ duration,
486
+ animation: `${duration}ms ${timing$1} ${name}`
487
+ };
488
+ }
489
+ const timing = typeof anim === "object" ? anim.timing ?? "" : "";
490
+ return {
491
+ duration,
492
+ animation: `${duration}ms ${timing} ${name}`
493
+ };
494
+ }
495
+ return void 0;
496
+ }
343
497
 
344
498
  //#endregion
345
499
  //#region src/components/NotFound.tsx
@@ -361,27 +515,17 @@ function NotFoundPage(props) {
361
515
  fontSize: "1rem",
362
516
  marginBottom: "0.5rem"
363
517
  },
364
- children: "This page does not exist"
518
+ children: "404 - This page does not exist"
365
519
  })
366
520
  });
367
521
  }
368
522
 
369
523
  //#endregion
370
- //#region src/errors/Redirection.ts
371
- var Redirection = class extends Error {
372
- page;
373
- constructor(page) {
374
- super("Redirection");
375
- this.page = page;
376
- }
377
- };
378
-
379
- //#endregion
380
- //#region src/providers/PageDescriptorProvider.ts
381
- const envSchema$1 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
382
- var PageDescriptorProvider = class {
383
- log = (0, __alepha_core.$logger)();
384
- env = (0, __alepha_core.$env)(envSchema$1);
524
+ //#region src/providers/ReactPageProvider.ts
525
+ const envSchema$2 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
526
+ var ReactPageProvider = class {
527
+ log = (0, __alepha_logger.$logger)();
528
+ env = (0, __alepha_core.$env)(envSchema$2);
385
529
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
386
530
  pages = [];
387
531
  getPages() {
@@ -408,22 +552,29 @@ var PageDescriptorProvider = class {
408
552
  return url.replace(/\/\/+/g, "/") || "/";
409
553
  }
410
554
  url(name, options = {}) {
411
- return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
555
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
412
556
  }
413
- root(state, context) {
414
- const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(RouterContext.Provider, { value: {
415
- state,
416
- context
417
- } }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element)));
557
+ root(state) {
558
+ const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element));
418
559
  if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
419
560
  return root;
420
561
  }
421
- async createLayers(route, request) {
422
- const { pathname, search } = request.url;
423
- const layers = [];
562
+ convertStringObjectToObject = (schema, value) => {
563
+ if (__alepha_core.TypeGuard.IsObject(schema) && typeof value === "object") {
564
+ for (const key in schema.properties) if (__alepha_core.TypeGuard.IsObject(schema.properties[key]) && typeof value[key] === "string") try {
565
+ value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
566
+ } catch (e) {}
567
+ }
568
+ return value;
569
+ };
570
+ /**
571
+ * Create a new RouterState based on a given route and request.
572
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
573
+ * It also handles errors and redirects.
574
+ */
575
+ async createLayers(route, state, previous = []) {
424
576
  let context = {};
425
577
  const stack = [{ route }];
426
- request.onError = (error) => this.renderError(error);
427
578
  let parent = route.parent;
428
579
  while (parent) {
429
580
  stack.unshift({ route: parent });
@@ -435,19 +586,19 @@ var PageDescriptorProvider = class {
435
586
  const route$1 = it.route;
436
587
  const config = {};
437
588
  try {
438
- config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
589
+ this.convertStringObjectToObject(route$1.schema?.query, state.query);
590
+ config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
439
591
  } catch (e) {
440
592
  it.error = e;
441
593
  break;
442
594
  }
443
595
  try {
444
- config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
596
+ config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
445
597
  } catch (e) {
446
598
  it.error = e;
447
599
  break;
448
600
  }
449
601
  it.config = { ...config };
450
- const previous = request.previous;
451
602
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
452
603
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
453
604
  const prev = JSON.stringify({
@@ -473,7 +624,7 @@ var PageDescriptorProvider = class {
473
624
  if (!route$1.resolve) continue;
474
625
  try {
475
626
  const props = await route$1.resolve?.({
476
- ...request,
627
+ ...state,
477
628
  ...config,
478
629
  ...context
479
630
  }) ?? {};
@@ -483,11 +634,8 @@ var PageDescriptorProvider = class {
483
634
  ...props
484
635
  };
485
636
  } catch (e) {
486
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
487
- pathname,
488
- search
489
- });
490
- this.log.error(e);
637
+ if (e instanceof Redirection) return { redirect: e.redirect };
638
+ this.log.error("Page resolver has failed", e);
491
639
  it.error = e;
492
640
  break;
493
641
  }
@@ -503,69 +651,58 @@ var PageDescriptorProvider = class {
503
651
  const path = acc.replace(/\/+/, "/");
504
652
  const localErrorHandler = this.getErrorHandler(it.route);
505
653
  if (localErrorHandler) {
506
- const onErrorParent = request.onError;
507
- request.onError = (error, context$1) => {
654
+ const onErrorParent = state.onError;
655
+ state.onError = (error, context$1) => {
508
656
  const result = localErrorHandler(error, context$1);
509
657
  if (result === void 0) return onErrorParent(error, context$1);
510
658
  return result;
511
659
  };
512
660
  }
513
- if (it.error) try {
514
- let element$1 = await request.onError(it.error, request);
515
- if (element$1 === void 0) throw it.error;
516
- if (element$1 instanceof Redirection) return this.createRedirectionLayer(element$1.page, {
517
- pathname,
518
- search
661
+ if (!it.error) try {
662
+ const element = await this.createElement(it.route, {
663
+ ...props,
664
+ ...context
519
665
  });
520
- if (element$1 === null) element$1 = this.renderError(it.error);
521
- layers.push({
666
+ state.layers.push({
667
+ name: it.route.name,
668
+ props,
669
+ part: it.route.path,
670
+ config: it.config,
671
+ element: this.renderView(i + 1, path, element, it.route),
672
+ index: i + 1,
673
+ path,
674
+ route: it.route,
675
+ cache: it.cache
676
+ });
677
+ } catch (e) {
678
+ it.error = e;
679
+ }
680
+ if (it.error) try {
681
+ let element = await state.onError(it.error, state);
682
+ if (element === void 0) throw it.error;
683
+ if (element instanceof Redirection) return { redirect: element.redirect };
684
+ if (element === null) element = this.renderError(it.error);
685
+ state.layers.push({
522
686
  props,
523
687
  error: it.error,
524
688
  name: it.route.name,
525
689
  part: it.route.path,
526
690
  config: it.config,
527
- element: this.renderView(i + 1, path, element$1, it.route),
691
+ element: this.renderView(i + 1, path, element, it.route),
528
692
  index: i + 1,
529
693
  path,
530
694
  route: it.route
531
695
  });
532
696
  break;
533
697
  } catch (e) {
534
- if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
535
- pathname,
536
- search
537
- });
698
+ if (e instanceof Redirection) return { redirect: e.redirect };
538
699
  throw e;
539
700
  }
540
- const element = await this.createElement(it.route, {
541
- ...props,
542
- ...context
543
- });
544
- layers.push({
545
- name: it.route.name,
546
- props,
547
- part: it.route.path,
548
- config: it.config,
549
- element: this.renderView(i + 1, path, element, it.route),
550
- index: i + 1,
551
- path,
552
- route: it.route,
553
- cache: it.cache
554
- });
555
701
  }
556
- return {
557
- layers,
558
- pathname,
559
- search
560
- };
702
+ return { state };
561
703
  }
562
- createRedirectionLayer(href, context) {
563
- return {
564
- layers: [],
565
- redirect: typeof href === "string" ? href : this.href(href),
566
- pathname: context.pathname,
567
- search: context.search
568
- };
704
+ createRedirectionLayer(redirect) {
705
+ return { redirect };
569
706
  }
570
707
  getErrorHandler(route) {
571
708
  if (route.errorHandler) return route.errorHandler;
@@ -576,6 +713,7 @@ var PageDescriptorProvider = class {
576
713
  }
577
714
  }
578
715
  async createElement(page, props) {
716
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
579
717
  if (page.lazy) {
580
718
  const component = await page.lazy();
581
719
  return (0, react.createElement)(component.default, props);
@@ -584,7 +722,7 @@ var PageDescriptorProvider = class {
584
722
  return void 0;
585
723
  }
586
724
  renderError(error) {
587
- return (0, react.createElement)(ErrorViewer_default, {
725
+ return (0, react.createElement)(ErrorViewer, {
588
726
  error,
589
727
  alepha: this.alepha
590
728
  });
@@ -610,7 +748,7 @@ var PageDescriptorProvider = class {
610
748
  }
611
749
  renderView(index, path, view, page) {
612
750
  view ??= this.renderEmptyView();
613
- const element = page.client ? (0, react.createElement)(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
751
+ const element = page.client ? (0, react.createElement)(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
614
752
  return (0, react.createElement)(RouterLayerContext.Provider, { value: {
615
753
  index,
616
754
  path
@@ -691,218 +829,9 @@ const isPageRoute = (it) => {
691
829
  return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
692
830
  };
693
831
 
694
- //#endregion
695
- //#region src/providers/BrowserRouterProvider.ts
696
- var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
697
- log = (0, __alepha_core.$logger)();
698
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
699
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
700
- add(entry) {
701
- this.pageDescriptorProvider.add(entry);
702
- }
703
- configure = (0, __alepha_core.$hook)({
704
- on: "configure",
705
- handler: async () => {
706
- for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
707
- path: page.match,
708
- page
709
- });
710
- }
711
- });
712
- async transition(url, options = {}) {
713
- const { pathname, search } = url;
714
- const state = {
715
- pathname,
716
- search,
717
- layers: []
718
- };
719
- const context = {
720
- url,
721
- query: {},
722
- params: {},
723
- onError: () => null,
724
- ...options.context ?? {}
725
- };
726
- await this.alepha.emit("react:transition:begin", {
727
- state,
728
- context
729
- });
730
- try {
731
- const previous = options.previous;
732
- const { route, params } = this.match(pathname);
733
- const query = {};
734
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
735
- context.query = query;
736
- context.params = params ?? {};
737
- context.previous = previous;
738
- if (isPageRoute(route)) {
739
- const result = await this.pageDescriptorProvider.createLayers(route.page, context);
740
- if (result.redirect) return {
741
- redirect: result.redirect,
742
- state,
743
- context
744
- };
745
- state.layers = result.layers;
746
- }
747
- if (state.layers.length === 0) state.layers.push({
748
- name: "not-found",
749
- element: (0, react.createElement)(NotFoundPage),
750
- index: 0,
751
- path: "/"
752
- });
753
- await this.alepha.emit("react:transition:success", {
754
- state,
755
- context
756
- });
757
- } catch (e) {
758
- this.log.error(e);
759
- state.layers = [{
760
- name: "error",
761
- element: this.pageDescriptorProvider.renderError(e),
762
- index: 0,
763
- path: "/"
764
- }];
765
- await this.alepha.emit("react:transition:error", {
766
- error: e,
767
- state,
768
- context
769
- });
770
- }
771
- if (options.state) {
772
- options.state.layers = state.layers;
773
- options.state.pathname = state.pathname;
774
- options.state.search = state.search;
775
- }
776
- if (options.previous) for (let i = 0; i < options.previous.length; i++) {
777
- const layer = options.previous[i];
778
- if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
779
- }
780
- await this.alepha.emit("react:transition:end", {
781
- state: options.state,
782
- context
783
- });
784
- return {
785
- context,
786
- state
787
- };
788
- }
789
- root(state, context) {
790
- return this.pageDescriptorProvider.root(state, context);
791
- }
792
- };
793
-
794
- //#endregion
795
- //#region src/providers/ReactBrowserProvider.ts
796
- var ReactBrowserProvider = class {
797
- log = (0, __alepha_core.$logger)();
798
- client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
799
- alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
800
- router = (0, __alepha_core.$inject)(BrowserRouterProvider);
801
- root;
802
- transitioning;
803
- state = {
804
- layers: [],
805
- pathname: "",
806
- search: ""
807
- };
808
- get document() {
809
- return window.document;
810
- }
811
- get history() {
812
- return window.history;
813
- }
814
- get location() {
815
- return window.location;
816
- }
817
- get url() {
818
- let url = this.location.pathname + this.location.search;
819
- return url;
820
- }
821
- pushState(url, replace) {
822
- let path = url;
823
- if (replace) this.history.replaceState({}, "", path);
824
- else this.history.pushState({}, "", path);
825
- }
826
- async invalidate(props) {
827
- const previous = [];
828
- if (props) {
829
- const [key] = Object.keys(props);
830
- const value = props[key];
831
- for (const layer of this.state.layers) {
832
- if (layer.props?.[key]) {
833
- previous.push({
834
- ...layer,
835
- props: {
836
- ...layer.props,
837
- [key]: value
838
- }
839
- });
840
- break;
841
- }
842
- previous.push(layer);
843
- }
844
- }
845
- await this.render({ previous });
846
- }
847
- async go(url, options = {}) {
848
- const result = await this.render({ url });
849
- if (result.context.url.pathname + result.context.url.search !== url) {
850
- this.pushState(result.context.url.pathname + result.context.url.search);
851
- return;
852
- }
853
- this.pushState(url, options.replace);
854
- }
855
- async render(options = {}) {
856
- const previous = options.previous ?? this.state.layers;
857
- const url = options.url ?? this.url;
858
- this.transitioning = { to: url };
859
- const result = await this.router.transition(new URL(`http://localhost${url}`), {
860
- previous,
861
- state: this.state
862
- });
863
- if (result.redirect) return await this.render({ url: result.redirect });
864
- this.transitioning = void 0;
865
- return result;
866
- }
867
- /**
868
- * Get embedded layers from the server.
869
- */
870
- getHydrationState() {
871
- try {
872
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
873
- } catch (error) {
874
- console.error(error);
875
- }
876
- }
877
- ready = (0, __alepha_core.$hook)({
878
- on: "ready",
879
- handler: async () => {
880
- const hydration = this.getHydrationState();
881
- const previous = hydration?.layers ?? [];
882
- if (hydration) {
883
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
884
- }
885
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
886
- ...link,
887
- prefix: hydration.links.prefix
888
- });
889
- const { context } = await this.render({ previous });
890
- await this.alepha.emit("react:browser:render", {
891
- state: this.state,
892
- context,
893
- hydration
894
- });
895
- window.addEventListener("popstate", () => {
896
- if (this.state.pathname === this.url) return;
897
- this.render();
898
- });
899
- }
900
- });
901
- };
902
-
903
832
  //#endregion
904
833
  //#region src/providers/ReactServerProvider.ts
905
- const envSchema = __alepha_core.t.object({
834
+ const envSchema$1 = __alepha_core.t.object({
906
835
  REACT_SERVER_DIST: __alepha_core.t.string({ default: "public" }),
907
836
  REACT_SERVER_PREFIX: __alepha_core.t.string({ default: "" }),
908
837
  REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
@@ -910,13 +839,14 @@ const envSchema = __alepha_core.t.object({
910
839
  REACT_SERVER_TEMPLATE: __alepha_core.t.optional(__alepha_core.t.string({ size: "rich" }))
911
840
  });
912
841
  var ReactServerProvider = class {
913
- log = (0, __alepha_core.$logger)();
842
+ log = (0, __alepha_logger.$logger)();
914
843
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
915
- pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
844
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
845
+ serverProvider = (0, __alepha_core.$inject)(__alepha_server.ServerProvider);
916
846
  serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
917
847
  serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
918
848
  serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
919
- env = (0, __alepha_core.$env)(envSchema);
849
+ env = (0, __alepha_core.$env)(envSchema$1);
920
850
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
921
851
  onConfigure = (0, __alepha_core.$hook)({
922
852
  on: "configure",
@@ -924,7 +854,23 @@ var ReactServerProvider = class {
924
854
  const pages = this.alepha.descriptors($page);
925
855
  const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
926
856
  this.alepha.state("react.server.ssr", ssrEnabled);
927
- for (const page of pages) page.render = this.createRenderFunction(page.name);
857
+ for (const page of pages) {
858
+ page.render = this.createRenderFunction(page.name);
859
+ page.fetch = async (options) => {
860
+ const response = await fetch(`${this.serverProvider.hostname}/${page.pathname(options)}`);
861
+ const html = await response.text();
862
+ if (options?.html) return {
863
+ html,
864
+ response
865
+ };
866
+ const match = html.match(this.ROOT_DIV_REGEX);
867
+ if (match) return {
868
+ html: match[3],
869
+ response
870
+ };
871
+ throw new __alepha_core.AlephaError("Invalid HTML response");
872
+ };
873
+ }
928
874
  if (this.alepha.isServerless() === "vite") {
929
875
  await this.configureVite(ssrEnabled);
930
876
  return;
@@ -963,7 +909,7 @@ var ReactServerProvider = class {
963
909
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
964
910
  }
965
911
  async registerPages(templateLoader) {
966
- for (const page of this.pageDescriptorProvider.getPages()) {
912
+ for (const page of this.pageApi.getPages()) {
967
913
  if (page.children?.length) continue;
968
914
  this.log.debug(`+ ${page.match} -> ${page.name}`);
969
915
  this.serverRouterProvider.createRoute({
@@ -997,25 +943,39 @@ var ReactServerProvider = class {
997
943
  */
998
944
  createRenderFunction(name, withIndex = false) {
999
945
  return async (options = {}) => {
1000
- const page = this.pageDescriptorProvider.page(name);
1001
- const url = new URL(this.pageDescriptorProvider.url(name, options));
1002
- const context = {
946
+ const page = this.pageApi.page(name);
947
+ const url = new URL(this.pageApi.url(name, options));
948
+ const entry = {
1003
949
  url,
1004
950
  params: options.params ?? {},
1005
951
  query: options.query ?? {},
1006
- head: {},
1007
- onError: () => null
952
+ onError: () => null,
953
+ layers: [],
954
+ meta: {}
1008
955
  };
1009
- await this.alepha.emit("react:server:render:begin", { context });
1010
- const state = await this.pageDescriptorProvider.createLayers(page, context);
1011
- if (!withIndex && !options.html) return {
1012
- context,
1013
- html: (0, react_dom_server.renderToString)(this.pageDescriptorProvider.root(state, context))
956
+ const state = entry;
957
+ this.log.trace("Rendering", { url });
958
+ await this.alepha.emit("react:server:render:begin", { state });
959
+ const { redirect } = await this.pageApi.createLayers(page, state);
960
+ if (redirect) return {
961
+ state,
962
+ html: "",
963
+ redirect
964
+ };
965
+ if (!withIndex && !options.html) {
966
+ this.alepha.state("react.router.state", state);
967
+ return {
968
+ state,
969
+ html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
970
+ };
971
+ }
972
+ const html = this.renderToHtml(this.template ?? "", state, options.hydration);
973
+ if (html instanceof Redirection) return {
974
+ state,
975
+ html: "",
976
+ redirect
1014
977
  };
1015
- const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
1016
- if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
1017
978
  const result = {
1018
- context,
1019
979
  state,
1020
980
  html
1021
981
  };
@@ -1023,89 +983,82 @@ var ReactServerProvider = class {
1023
983
  return result;
1024
984
  };
1025
985
  }
1026
- createHandler(page, templateLoader) {
986
+ createHandler(route, templateLoader) {
1027
987
  return async (serverRequest) => {
1028
988
  const { url, reply, query, params } = serverRequest;
1029
989
  const template = await templateLoader();
1030
990
  if (!template) throw new Error("Template not found");
1031
- this.log.trace("Rendering page", { name: page.name });
1032
- const context = {
991
+ this.log.trace("Rendering page", { name: route.name });
992
+ const entry = {
1033
993
  url,
1034
994
  params,
1035
995
  query,
1036
- head: {},
1037
- onError: () => null
996
+ onError: () => null,
997
+ layers: []
1038
998
  };
1039
- if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) {
1040
- const srv = this.alepha.inject(__alepha_server_links.ServerLinksProvider);
1041
- const schema = __alepha_server.apiLinksResponseSchema;
1042
- context.links = this.alepha.parse(schema, await srv.getLinks({
1043
- user: serverRequest.user,
1044
- authorization: serverRequest.headers.authorization
1045
- }));
1046
- this.alepha.context.set("links", context.links);
1047
- }
1048
- let target = page;
999
+ const state = entry;
1000
+ if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
1001
+ user: serverRequest.user,
1002
+ authorization: serverRequest.headers.authorization
1003
+ }));
1004
+ let target = route;
1049
1005
  while (target) {
1050
- if (page.can && !page.can()) {
1006
+ if (route.can && !route.can()) {
1051
1007
  reply.status = 403;
1052
1008
  reply.headers["content-type"] = "text/plain";
1053
1009
  return "Forbidden";
1054
1010
  }
1055
1011
  target = target.parent;
1056
1012
  }
1057
- await this.alepha.emit("react:transition:begin", {
1058
- request: serverRequest,
1059
- context
1060
- });
1061
1013
  await this.alepha.emit("react:server:render:begin", {
1062
1014
  request: serverRequest,
1063
- context
1015
+ state
1064
1016
  });
1065
1017
  this.serverTimingProvider.beginTiming("createLayers");
1066
- const state = await this.pageDescriptorProvider.createLayers(page, context);
1018
+ const { redirect } = await this.pageApi.createLayers(route, state);
1067
1019
  this.serverTimingProvider.endTiming("createLayers");
1068
- if (state.redirect) return reply.redirect(state.redirect);
1020
+ if (redirect) return reply.redirect(redirect);
1069
1021
  reply.headers["content-type"] = "text/html";
1070
1022
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
1071
1023
  reply.headers.pragma = "no-cache";
1072
1024
  reply.headers.expires = "0";
1073
- if (page.cache && serverRequest.user) delete context.links;
1074
- const html = this.renderToHtml(template, state, context);
1025
+ const html = this.renderToHtml(template, state);
1075
1026
  if (html instanceof Redirection) {
1076
- reply.redirect(typeof html.page === "string" ? html.page : this.pageDescriptorProvider.href(html.page));
1027
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
1077
1028
  return;
1078
1029
  }
1079
1030
  const event = {
1080
1031
  request: serverRequest,
1081
- context,
1082
1032
  state,
1083
1033
  html
1084
1034
  };
1085
1035
  await this.alepha.emit("react:server:render:end", event);
1086
- page.onServerResponse?.(serverRequest);
1087
- this.log.trace("Page rendered", { name: page.name });
1036
+ route.onServerResponse?.(serverRequest);
1037
+ this.log.trace("Page rendered", { name: route.name });
1088
1038
  return event.html;
1089
1039
  };
1090
1040
  }
1091
- renderToHtml(template, state, context, hydration = true) {
1092
- const element = this.pageDescriptorProvider.root(state, context);
1041
+ renderToHtml(template, state, hydration = true) {
1042
+ const element = this.pageApi.root(state);
1043
+ this.alepha.state("react.router.state", state);
1093
1044
  this.serverTimingProvider.beginTiming("renderToString");
1094
1045
  let app = "";
1095
1046
  try {
1096
1047
  app = (0, react_dom_server.renderToString)(element);
1097
1048
  } catch (error) {
1098
- this.log.error("Error during SSR", error);
1099
- const element$1 = context.onError(error, context);
1049
+ this.log.error("renderToString has failed, fallback to error handler", error);
1050
+ const element$1 = state.onError(error, state);
1100
1051
  if (element$1 instanceof Redirection) return element$1;
1101
1052
  app = (0, react_dom_server.renderToString)(element$1);
1053
+ this.log.debug("Error handled successfully with fallback");
1102
1054
  }
1103
1055
  this.serverTimingProvider.endTiming("renderToString");
1104
1056
  const response = { html: template };
1105
1057
  if (hydration) {
1106
- const { request, context: context$1,...rest } = this.alepha.context.als?.getStore() ?? {};
1058
+ const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
1107
1059
  const hydrationData = {
1108
- ...rest,
1060
+ ...store,
1061
+ "react.router.state": void 0,
1109
1062
  layers: state.layers.map((it) => ({
1110
1063
  ...it,
1111
1064
  error: it.error ? {
@@ -1120,7 +1073,7 @@ var ReactServerProvider = class {
1120
1073
  route: void 0
1121
1074
  }))
1122
1075
  };
1123
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
1076
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1124
1077
  this.fillTemplate(response, app, script);
1125
1078
  }
1126
1079
  return response.html;
@@ -1141,27 +1094,262 @@ var ReactServerProvider = class {
1141
1094
  };
1142
1095
 
1143
1096
  //#endregion
1144
- //#region src/hooks/RouterHookApi.ts
1145
- var RouterHookApi = class {
1146
- constructor(pages, context, state, layer, pageApi, browser) {
1147
- this.pages = pages;
1148
- this.context = context;
1149
- this.state = state;
1150
- this.layer = layer;
1151
- this.pageApi = pageApi;
1152
- this.browser = browser;
1097
+ //#region src/providers/ReactBrowserRouterProvider.ts
1098
+ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1099
+ log = (0, __alepha_logger.$logger)();
1100
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1101
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1102
+ add(entry) {
1103
+ this.pageApi.add(entry);
1104
+ }
1105
+ configure = (0, __alepha_core.$hook)({
1106
+ on: "configure",
1107
+ handler: async () => {
1108
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
1109
+ path: page.match,
1110
+ page
1111
+ });
1112
+ }
1113
+ });
1114
+ async transition(url, previous = [], meta = {}) {
1115
+ const { pathname, search } = url;
1116
+ const entry = {
1117
+ url,
1118
+ query: {},
1119
+ params: {},
1120
+ layers: [],
1121
+ onError: () => null,
1122
+ meta
1123
+ };
1124
+ const state = entry;
1125
+ await this.alepha.emit("react:transition:begin", {
1126
+ previous: this.alepha.state("react.router.state"),
1127
+ state
1128
+ });
1129
+ try {
1130
+ const { route, params } = this.match(pathname);
1131
+ const query = {};
1132
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
1133
+ state.query = query;
1134
+ state.params = params ?? {};
1135
+ if (isPageRoute(route)) {
1136
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
1137
+ if (redirect) return redirect;
1138
+ }
1139
+ if (state.layers.length === 0) state.layers.push({
1140
+ name: "not-found",
1141
+ element: (0, react.createElement)(NotFoundPage),
1142
+ index: 0,
1143
+ path: "/"
1144
+ });
1145
+ await this.alepha.emit("react:transition:success", { state });
1146
+ } catch (e) {
1147
+ this.log.error("Transition has failed", e);
1148
+ state.layers = [{
1149
+ name: "error",
1150
+ element: this.pageApi.renderError(e),
1151
+ index: 0,
1152
+ path: "/"
1153
+ }];
1154
+ await this.alepha.emit("react:transition:error", {
1155
+ error: e,
1156
+ state
1157
+ });
1158
+ }
1159
+ if (previous) for (let i = 0; i < previous.length; i++) {
1160
+ const layer = previous[i];
1161
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1162
+ }
1163
+ this.alepha.state("react.router.state", state);
1164
+ await this.alepha.emit("react:transition:end", { state });
1165
+ }
1166
+ root(state) {
1167
+ return this.pageApi.root(state);
1168
+ }
1169
+ };
1170
+
1171
+ //#endregion
1172
+ //#region src/providers/ReactBrowserProvider.ts
1173
+ const envSchema = __alepha_core.t.object({ REACT_ROOT_ID: __alepha_core.t.string({ default: "root" }) });
1174
+ var ReactBrowserProvider = class {
1175
+ env = (0, __alepha_core.$env)(envSchema);
1176
+ log = (0, __alepha_logger.$logger)();
1177
+ client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
1178
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1179
+ router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
1180
+ dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
1181
+ options = { scrollRestoration: "top" };
1182
+ getRootElement() {
1183
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
1184
+ if (root) return root;
1185
+ const div = this.document.createElement("div");
1186
+ div.id = this.env.REACT_ROOT_ID;
1187
+ this.document.body.prepend(div);
1188
+ return div;
1189
+ }
1190
+ transitioning;
1191
+ get state() {
1192
+ return this.alepha.state("react.router.state");
1193
+ }
1194
+ /**
1195
+ * Accessor for Document DOM API.
1196
+ */
1197
+ get document() {
1198
+ return window.document;
1199
+ }
1200
+ /**
1201
+ * Accessor for History DOM API.
1202
+ */
1203
+ get history() {
1204
+ return window.history;
1205
+ }
1206
+ /**
1207
+ * Accessor for Location DOM API.
1208
+ */
1209
+ get location() {
1210
+ return window.location;
1211
+ }
1212
+ get base() {
1213
+ const base = {}.env?.BASE_URL;
1214
+ if (!base || base === "/") return "";
1215
+ return base;
1216
+ }
1217
+ get url() {
1218
+ const url = this.location.pathname + this.location.search;
1219
+ if (this.base) return url.replace(this.base, "");
1220
+ return url;
1221
+ }
1222
+ pushState(path, replace) {
1223
+ const url = this.base + path;
1224
+ if (replace) this.history.replaceState({}, "", url);
1225
+ else this.history.pushState({}, "", url);
1226
+ }
1227
+ async invalidate(props) {
1228
+ const previous = [];
1229
+ this.log.trace("Invalidating layers");
1230
+ if (props) {
1231
+ const [key] = Object.keys(props);
1232
+ const value = props[key];
1233
+ for (const layer of this.state.layers) {
1234
+ if (layer.props?.[key]) {
1235
+ previous.push({
1236
+ ...layer,
1237
+ props: {
1238
+ ...layer.props,
1239
+ [key]: value
1240
+ }
1241
+ });
1242
+ break;
1243
+ }
1244
+ previous.push(layer);
1245
+ }
1246
+ }
1247
+ await this.render({ previous });
1248
+ }
1249
+ async go(url, options = {}) {
1250
+ this.log.trace(`Going to ${url}`, {
1251
+ url,
1252
+ options
1253
+ });
1254
+ await this.render({
1255
+ url,
1256
+ previous: options.force ? [] : this.state.layers,
1257
+ meta: options.meta
1258
+ });
1259
+ if (this.state.url.pathname + this.state.url.search !== url) {
1260
+ this.pushState(this.state.url.pathname + this.state.url.search);
1261
+ return;
1262
+ }
1263
+ this.pushState(url, options.replace);
1264
+ }
1265
+ async render(options = {}) {
1266
+ const previous = options.previous ?? this.state.layers;
1267
+ const url = options.url ?? this.url;
1268
+ const start = this.dateTimeProvider.now();
1269
+ this.transitioning = {
1270
+ to: url,
1271
+ from: this.state?.url.pathname
1272
+ };
1273
+ this.log.debug("Transitioning...", { to: url });
1274
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
1275
+ if (redirect) {
1276
+ this.log.info("Redirecting to", { redirect });
1277
+ return await this.render({ url: redirect });
1278
+ }
1279
+ const ms = this.dateTimeProvider.now().diff(start);
1280
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1281
+ this.transitioning = void 0;
1282
+ }
1283
+ /**
1284
+ * Get embedded layers from the server.
1285
+ */
1286
+ getHydrationState() {
1287
+ try {
1288
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1289
+ } catch (error) {
1290
+ console.error(error);
1291
+ }
1292
+ }
1293
+ onTransitionEnd = (0, __alepha_core.$hook)({
1294
+ on: "react:transition:end",
1295
+ handler: () => {
1296
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
1297
+ this.log.trace("Restoring scroll position to top");
1298
+ window.scrollTo(0, 0);
1299
+ }
1300
+ }
1301
+ });
1302
+ ready = (0, __alepha_core.$hook)({
1303
+ on: "ready",
1304
+ handler: async () => {
1305
+ const hydration = this.getHydrationState();
1306
+ const previous = hydration?.layers ?? [];
1307
+ if (hydration) {
1308
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1309
+ }
1310
+ await this.render({ previous });
1311
+ const element = this.router.root(this.state);
1312
+ await this.alepha.emit("react:browser:render", {
1313
+ element,
1314
+ root: this.getRootElement(),
1315
+ hydration,
1316
+ state: this.state
1317
+ });
1318
+ window.addEventListener("popstate", () => {
1319
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1320
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1321
+ this.render();
1322
+ });
1323
+ }
1324
+ });
1325
+ };
1326
+
1327
+ //#endregion
1328
+ //#region src/services/ReactRouter.ts
1329
+ var ReactRouter = class {
1330
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1331
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1332
+ get state() {
1333
+ return this.alepha.state("react.router.state");
1334
+ }
1335
+ get pages() {
1336
+ return this.pageApi.getPages();
1337
+ }
1338
+ get browser() {
1339
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1340
+ return void 0;
1153
1341
  }
1154
1342
  path(name, config = {}) {
1155
1343
  return this.pageApi.pathname(name, {
1156
1344
  params: {
1157
- ...this.context.params,
1345
+ ...this.state.params,
1158
1346
  ...config.params
1159
1347
  },
1160
1348
  query: config.query
1161
1349
  });
1162
1350
  }
1163
1351
  getURL() {
1164
- if (!this.browser) return this.context.url;
1352
+ if (!this.browser) return this.state.url;
1165
1353
  return new URL(this.location.href);
1166
1354
  }
1167
1355
  get location() {
@@ -1172,11 +1360,11 @@ var RouterHookApi = class {
1172
1360
  return this.state;
1173
1361
  }
1174
1362
  get pathname() {
1175
- return this.state.pathname;
1363
+ return this.state.url.pathname;
1176
1364
  }
1177
1365
  get query() {
1178
1366
  const query = {};
1179
- for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
1367
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1180
1368
  return query;
1181
1369
  }
1182
1370
  async back() {
@@ -1188,17 +1376,6 @@ var RouterHookApi = class {
1188
1376
  async invalidate(props) {
1189
1377
  await this.browser?.invalidate(props);
1190
1378
  }
1191
- /**
1192
- * Create a valid href for the given pathname.
1193
- *
1194
- * @param pathname
1195
- * @param layer
1196
- */
1197
- createHref(pathname, layer = this.layer, options = {}) {
1198
- if (typeof pathname === "object") pathname = pathname.options.path ?? "";
1199
- if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
1200
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
1201
- }
1202
1379
  async go(path, options) {
1203
1380
  for (const page of this.pages) if (page.name === path) {
1204
1381
  await this.browser?.go(this.path(path, options), options);
@@ -1213,7 +1390,7 @@ var RouterHookApi = class {
1213
1390
  break;
1214
1391
  }
1215
1392
  return {
1216
- href,
1393
+ href: this.base(href),
1217
1394
  onClick: (ev) => {
1218
1395
  ev.stopPropagation();
1219
1396
  ev.preventDefault();
@@ -1221,6 +1398,11 @@ var RouterHookApi = class {
1221
1398
  }
1222
1399
  };
1223
1400
  }
1401
+ base(path) {
1402
+ const base = {}.env?.BASE_URL;
1403
+ if (!base || base === "/") return path;
1404
+ return base + path;
1405
+ }
1224
1406
  /**
1225
1407
  * Set query params.
1226
1408
  *
@@ -1236,107 +1418,99 @@ var RouterHookApi = class {
1236
1418
  }
1237
1419
  };
1238
1420
 
1421
+ //#endregion
1422
+ //#region src/hooks/useInject.ts
1423
+ /**
1424
+ * Hook to inject a service instance.
1425
+ * It's a wrapper of `useAlepha().inject(service)` with a memoization.
1426
+ */
1427
+ const useInject = (service) => {
1428
+ const alepha = useAlepha();
1429
+ return (0, react.useMemo)(() => alepha.inject(service), []);
1430
+ };
1431
+
1239
1432
  //#endregion
1240
1433
  //#region src/hooks/useRouter.ts
1434
+ /**
1435
+ * Use this hook to access the React Router instance.
1436
+ *
1437
+ * You can add a type parameter to specify the type of your application.
1438
+ * This will allow you to use the router in a typesafe way.
1439
+ *
1440
+ * @example
1441
+ * class App {
1442
+ * home = $page();
1443
+ * }
1444
+ *
1445
+ * const router = useRouter<App>();
1446
+ * router.go("home"); // typesafe
1447
+ */
1241
1448
  const useRouter = () => {
1242
- const alepha = useAlepha();
1243
- const ctx = (0, react.useContext)(RouterContext);
1244
- const layer = (0, react.useContext)(RouterLayerContext);
1245
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1246
- const pages = (0, react.useMemo)(() => {
1247
- return alepha.inject(PageDescriptorProvider).getPages();
1248
- }, []);
1249
- return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1449
+ return useInject(ReactRouter);
1250
1450
  };
1251
1451
 
1252
1452
  //#endregion
1253
1453
  //#region src/components/Link.tsx
1254
1454
  const Link = (props) => {
1255
1455
  const router = useRouter();
1256
- const { to,...anchorProps } = props;
1257
1456
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1258
- ...router.anchor(to),
1259
- ...anchorProps,
1457
+ ...props,
1458
+ ...router.anchor(props.href),
1260
1459
  children: props.children
1261
1460
  });
1262
1461
  };
1263
- var Link_default = Link;
1264
1462
 
1265
1463
  //#endregion
1266
1464
  //#region src/hooks/useActive.ts
1267
- const useActive = (path) => {
1465
+ const useActive = (args) => {
1268
1466
  const router = useRouter();
1269
- const ctx = (0, react.useContext)(RouterContext);
1270
- const layer = (0, react.useContext)(RouterLayerContext);
1271
- if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1272
- const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
1273
- const href = (0, react.useMemo)(() => router.createHref(path ?? "", layer), [path, layer]);
1274
1467
  const [isPending, setPending] = (0, react.useState)(false);
1275
- const isActive = current === href || current === `${href}/` || `${current}/` === href;
1276
- useRouterEvents({ onEnd: ({ state }) => {
1277
- path && setCurrent(state.pathname);
1278
- } }, [path]);
1468
+ const state = useRouterState();
1469
+ const current = state.url.pathname;
1470
+ const options = typeof args === "string" ? { href: args } : {
1471
+ ...args,
1472
+ href: args.href
1473
+ };
1474
+ const href = options.href;
1475
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
1476
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
1279
1477
  return {
1280
1478
  isPending,
1281
1479
  isActive,
1282
1480
  anchorProps: {
1283
- href,
1284
- onClick: (ev) => {
1481
+ href: router.base(href),
1482
+ onClick: async (ev) => {
1285
1483
  ev?.stopPropagation();
1286
1484
  ev?.preventDefault();
1287
1485
  if (isActive) return;
1288
1486
  if (isPending) return;
1289
1487
  setPending(true);
1290
- router.go(href).then(() => {
1488
+ try {
1489
+ await router.go(href);
1490
+ } finally {
1291
1491
  setPending(false);
1292
- });
1492
+ }
1293
1493
  }
1294
1494
  }
1295
1495
  };
1296
1496
  };
1297
1497
 
1298
1498
  //#endregion
1299
- //#region src/hooks/useInject.ts
1300
- const useInject = (service) => {
1301
- const alepha = useAlepha();
1302
- return (0, react.useMemo)(() => alepha.inject(service), []);
1303
- };
1304
-
1305
- //#endregion
1306
- //#region src/hooks/useStore.ts
1499
+ //#region src/hooks/useClient.ts
1307
1500
  /**
1308
- * Hook to access and mutate the Alepha state.
1501
+ * Hook to get a virtual client for the specified scope.
1502
+ *
1503
+ * It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
1309
1504
  */
1310
- const useStore = (key, defaultValue) => {
1311
- const alepha = useAlepha();
1312
- (0, react.useMemo)(() => {
1313
- if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1314
- }, [defaultValue]);
1315
- const [state, setState] = (0, react.useState)(alepha.state(key));
1316
- (0, react.useEffect)(() => {
1317
- if (!alepha.isBrowser()) return;
1318
- return alepha.on("state:mutate", (ev) => {
1319
- if (ev.key === key) setState(ev.value);
1320
- });
1321
- }, []);
1322
- if (!alepha.isBrowser()) {
1323
- const value = alepha.context.get(key);
1324
- if (value !== null) return [value, (_) => {}];
1325
- }
1326
- return [state, (value) => {
1327
- alepha.state(key, value);
1328
- }];
1329
- };
1330
-
1331
- //#endregion
1332
- //#region src/hooks/useClient.ts
1333
- const useClient = (_scope) => {
1334
- useStore("user");
1335
- return useInject(__alepha_server_links.LinkProvider).client();
1505
+ const useClient = (scope) => {
1506
+ return useInject(__alepha_server_links.LinkProvider).client(scope);
1336
1507
  };
1337
1508
 
1338
1509
  //#endregion
1339
1510
  //#region src/hooks/useQueryParams.ts
1511
+ /**
1512
+ * Not well tested. Use with caution.
1513
+ */
1340
1514
  const useQueryParams = (schema, options = {}) => {
1341
1515
  const alepha = useAlepha();
1342
1516
  const key = options.key ?? "q";
@@ -1367,29 +1541,17 @@ const decode = (alepha, schema, data) => {
1367
1541
  }
1368
1542
  };
1369
1543
 
1370
- //#endregion
1371
- //#region src/hooks/useRouterState.ts
1372
- const useRouterState = () => {
1373
- const router = (0, react.useContext)(RouterContext);
1374
- const layer = (0, react.useContext)(RouterLayerContext);
1375
- if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
1376
- const [state, setState] = (0, react.useState)(router.state);
1377
- useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
1378
- return state;
1379
- };
1380
-
1381
1544
  //#endregion
1382
1545
  //#region src/hooks/useSchema.ts
1383
1546
  const useSchema = (action) => {
1384
1547
  const name = action.name;
1385
1548
  const alepha = useAlepha();
1386
1549
  const httpClient = useInject(__alepha_server.HttpClient);
1387
- const linkProvider = useInject(__alepha_server_links.LinkProvider);
1388
1550
  const [schema, setSchema] = (0, react.useState)(ssrSchemaLoading(alepha, name));
1389
1551
  (0, react.useEffect)(() => {
1390
1552
  if (!schema.loading) return;
1391
1553
  const opts = { cache: true };
1392
- httpClient.fetch(`${linkProvider.URL_LINKS}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1554
+ httpClient.fetch(`${__alepha_server_links.LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
1393
1555
  }, [name]);
1394
1556
  return schema;
1395
1557
  };
@@ -1398,10 +1560,10 @@ const useSchema = (action) => {
1398
1560
  */
1399
1561
  const ssrSchemaLoading = (alepha, name) => {
1400
1562
  if (!alepha.isBrowser()) {
1401
- const links = alepha.context.get("links")?.links ?? [];
1402
- const can = links.find((it) => it.name === name);
1563
+ const linkProvider = alepha.inject(__alepha_server_links.LinkProvider);
1564
+ const can = linkProvider.getServerLinks().find((link) => link.name === name);
1403
1565
  if (can) {
1404
- const schema$1 = alepha.inject(__alepha_server_links.LinkProvider).links?.find((it) => it.name === name)?.schema;
1566
+ const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
1405
1567
  if (schema$1) {
1406
1568
  can.schema = schema$1;
1407
1569
  return schema$1;
@@ -1409,7 +1571,7 @@ const ssrSchemaLoading = (alepha, name) => {
1409
1571
  }
1410
1572
  return { loading: true };
1411
1573
  }
1412
- const schema = alepha.inject(__alepha_server_links.LinkProvider).links?.find((it) => it.name === name)?.schema;
1574
+ const schema = alepha.inject(__alepha_server_links.LinkProvider).links.find((it) => it.name === name)?.schema;
1413
1575
  if (schema) return schema;
1414
1576
  return { loading: true };
1415
1577
  };
@@ -1431,28 +1593,28 @@ const AlephaReact = (0, __alepha_core.$module)({
1431
1593
  descriptors: [$page],
1432
1594
  services: [
1433
1595
  ReactServerProvider,
1434
- PageDescriptorProvider,
1435
- ReactBrowserProvider
1596
+ ReactPageProvider,
1597
+ ReactRouter
1436
1598
  ],
1437
- register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider)
1599
+ register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
1438
1600
  });
1439
1601
 
1440
1602
  //#endregion
1441
1603
  exports.$page = $page;
1442
1604
  exports.AlephaContext = AlephaContext;
1443
1605
  exports.AlephaReact = AlephaReact;
1444
- exports.ClientOnly = ClientOnly_default;
1445
- exports.ErrorBoundary = ErrorBoundary_default;
1446
- exports.Link = Link_default;
1606
+ exports.ClientOnly = ClientOnly;
1607
+ exports.ErrorBoundary = ErrorBoundary;
1608
+ exports.ErrorViewer = ErrorViewer;
1609
+ exports.Link = Link;
1447
1610
  exports.NestedView = NestedView_default;
1448
1611
  exports.NotFound = NotFoundPage;
1449
1612
  exports.PageDescriptor = PageDescriptor;
1450
- exports.PageDescriptorProvider = PageDescriptorProvider;
1451
1613
  exports.ReactBrowserProvider = ReactBrowserProvider;
1614
+ exports.ReactPageProvider = ReactPageProvider;
1615
+ exports.ReactRouter = ReactRouter;
1452
1616
  exports.ReactServerProvider = ReactServerProvider;
1453
1617
  exports.Redirection = Redirection;
1454
- exports.RouterContext = RouterContext;
1455
- exports.RouterHookApi = RouterHookApi;
1456
1618
  exports.RouterLayerContext = RouterLayerContext;
1457
1619
  exports.isPageRoute = isPageRoute;
1458
1620
  exports.ssrSchemaLoading = ssrSchemaLoading;