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