@alepha/react 0.5.1 → 0.6.0

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.
@@ -1,5 +1,4 @@
1
1
  import { $inject, $logger, Alepha, EventEmitter } from "@alepha/core";
2
- import type { UserAccountInfo } from "@alepha/security";
3
2
  import type { MatchFunction, ParamData } from "path-to-regexp";
4
3
  import { compile, match } from "path-to-regexp";
5
4
  import type { ReactNode } from "react";
@@ -7,14 +6,8 @@ import { createElement } from "react";
7
6
  import NestedView from "../components/NestedView";
8
7
  import { RouterContext } from "../contexts/RouterContext";
9
8
  import { RouterLayerContext } from "../contexts/RouterLayerContext";
10
- import type { PageDescriptorOptions } from "../descriptors/$page";
11
- import type { HrefLike } from "../hooks/RouterHookApi";
12
-
13
- export class RedirectException extends Error {
14
- constructor(public readonly page: HrefLike) {
15
- super("Redirection");
16
- }
17
- }
9
+ import type { PageContext, PageDescriptorOptions } from "../descriptors/$page";
10
+ import { RedirectionError } from "../errors/RedirectionError";
18
11
 
19
12
  export class Router extends EventEmitter<RouterEvents> {
20
13
  protected readonly log = $logger();
@@ -40,10 +33,7 @@ export class Router extends EventEmitter<RouterEvents> {
40
33
  /**
41
34
  *
42
35
  */
43
- public root(
44
- state: RouterState,
45
- opts: { user?: UserAccountInfo } = {},
46
- ): ReactNode {
36
+ public root(state: RouterState, context: PageContext = {}): ReactNode {
47
37
  return createElement(
48
38
  RouterContext.Provider,
49
39
  {
@@ -51,7 +41,10 @@ export class Router extends EventEmitter<RouterEvents> {
51
41
  state,
52
42
  router: this,
53
43
  alepha: this.alepha,
54
- session: opts.user ? { user: opts.user } : undefined,
44
+ args: {
45
+ user: context.user,
46
+ cookies: context.cookies,
47
+ },
55
48
  },
56
49
  },
57
50
  state.layers[0]?.element,
@@ -66,22 +59,19 @@ export class Router extends EventEmitter<RouterEvents> {
66
59
  public async render(
67
60
  url: string,
68
61
  options: RouterRenderOptions = {},
69
- ): Promise<{
70
- element: ReactNode;
71
- layers: Layer[];
72
- redirect?: string;
73
- }> {
62
+ ): Promise<RouterRenderResult> {
74
63
  const [pathname, search = ""] = url.split("?");
75
64
  const state: RouterState = {
76
65
  pathname,
77
66
  search,
78
67
  layers: [],
68
+ context: {},
79
69
  };
80
70
 
81
- this.emit("begin", undefined);
71
+ await this.emit("begin", undefined);
82
72
 
83
73
  try {
84
- let layers = await this.match(url, options);
74
+ let layers = await this.match(url, options, state.context);
85
75
  if (layers.length === 0) {
86
76
  if (this.notFoundPageRoute) {
87
77
  layers = await this.createLayers(url, this.notFoundPageRoute);
@@ -96,14 +86,15 @@ export class Router extends EventEmitter<RouterEvents> {
96
86
  }
97
87
 
98
88
  state.layers = layers;
99
- this.emit("success", undefined);
89
+ await this.emit("success", undefined);
100
90
  } catch (e) {
101
- if (e instanceof RedirectException) {
91
+ if (e instanceof RedirectionError) {
102
92
  // redirect - stop processing
103
93
  return {
104
94
  element: null,
105
95
  layers: [],
106
96
  redirect: typeof e.page === "string" ? e.page : this.href(e.page),
97
+ context: state.context,
107
98
  };
108
99
  }
109
100
 
@@ -118,7 +109,7 @@ export class Router extends EventEmitter<RouterEvents> {
118
109
  },
119
110
  ];
120
111
 
121
- this.emit("error", e as Error);
112
+ await this.emit("error", e as Error);
122
113
  }
123
114
 
124
115
  if (options.state) {
@@ -126,20 +117,23 @@ export class Router extends EventEmitter<RouterEvents> {
126
117
  options.state.layers = state.layers;
127
118
  options.state.pathname = state.pathname;
128
119
  options.state.search = state.search;
120
+ options.state.context = state.context;
129
121
 
130
- this.emit("end", options.state);
122
+ await this.emit("end", options.state);
131
123
 
132
124
  return {
133
- element: this.root(options.state, options),
125
+ element: this.root(options.state, options.args),
134
126
  layers: options.state.layers,
127
+ context: state.context,
135
128
  };
136
129
  }
137
130
 
138
131
  // stateless (ssr)
139
- this.emit("end", state);
132
+ await this.emit("end", state);
140
133
  return {
141
- element: this.root(state, options),
134
+ element: this.root(state, options.args),
142
135
  layers: state.layers,
136
+ context: state.context,
143
137
  };
144
138
  }
145
139
 
@@ -147,11 +141,13 @@ export class Router extends EventEmitter<RouterEvents> {
147
141
  *
148
142
  * @param url
149
143
  * @param options
144
+ * @param context
150
145
  * @protected
151
146
  */
152
147
  public async match(
153
148
  url: string,
154
149
  options: RouterMatchOptions = {},
150
+ context: RouterRenderContext = {},
155
151
  ): Promise<Layer[]> {
156
152
  const pages = this.pages;
157
153
  const previous = options.previous;
@@ -178,7 +174,8 @@ export class Router extends EventEmitter<RouterEvents> {
178
174
  params,
179
175
  query,
180
176
  previous,
181
- options.user,
177
+ options.args,
178
+ context,
182
179
  );
183
180
  }
184
181
  }
@@ -189,11 +186,13 @@ export class Router extends EventEmitter<RouterEvents> {
189
186
  /**
190
187
  * Create layers for the given route.
191
188
  *
189
+ * @param url
192
190
  * @param route
193
191
  * @param params
194
192
  * @param query
195
193
  * @param previous
196
- * @param user
194
+ * @param args
195
+ * @param renderContext
197
196
  * @protected
198
197
  */
199
198
  public async createLayers(
@@ -202,7 +201,8 @@ export class Router extends EventEmitter<RouterEvents> {
202
201
  params: Record<string, any> = {},
203
202
  query: Record<string, string> = {},
204
203
  previous: PreviousLayerData[] = [],
205
- user?: UserAccountInfo,
204
+ args?: PageContext,
205
+ renderContext?: RouterRenderContext,
206
206
  ): Promise<Layer[]> {
207
207
  const layers: Layer[] = [];
208
208
  let context: Record<string, any> = {};
@@ -224,7 +224,7 @@ export class Router extends EventEmitter<RouterEvents> {
224
224
  try {
225
225
  config.query = route.schema?.query
226
226
  ? this.alepha.parse(route.schema.query, query)
227
- : {};
227
+ : query;
228
228
  } catch (e) {
229
229
  it.error = e as Error;
230
230
  break;
@@ -233,7 +233,7 @@ export class Router extends EventEmitter<RouterEvents> {
233
233
  try {
234
234
  config.params = route.schema?.params
235
235
  ? this.alepha.parse(route.schema.params, params)
236
- : {};
236
+ : params;
237
237
  } catch (e) {
238
238
  it.error = e as Error;
239
239
  break;
@@ -278,12 +278,15 @@ export class Router extends EventEmitter<RouterEvents> {
278
278
 
279
279
  try {
280
280
  const props =
281
- (await route.resolve?.({
282
- ...config,
283
- ...context,
284
- user,
285
- url,
286
- } as any)) ?? {};
281
+ (await route.resolve?.(
282
+ {
283
+ ...config,
284
+ ...context,
285
+ context: args,
286
+ url,
287
+ } as any,
288
+ args ?? {},
289
+ )) ?? {};
287
290
 
288
291
  // save props
289
292
  it.props = {
@@ -297,7 +300,7 @@ export class Router extends EventEmitter<RouterEvents> {
297
300
  };
298
301
  } catch (e) {
299
302
  // check if we need to redirect
300
- if (e instanceof RedirectException) {
303
+ if (e instanceof RedirectionError) {
301
304
  throw e; // redirect - stop processing
302
305
  }
303
306
 
@@ -318,6 +321,13 @@ export class Router extends EventEmitter<RouterEvents> {
318
321
  params[key] = String(params[key]);
319
322
  }
320
323
 
324
+ if (it.route.helmet && renderContext) {
325
+ this.mergeRenderContext(it.route, renderContext, {
326
+ ...props,
327
+ ...context,
328
+ });
329
+ }
330
+
321
331
  acc += "/";
322
332
  acc += it.route.path ? compile(it.route.path)(params) : "";
323
333
  const path = acc.replace(/\/+/, "/");
@@ -402,6 +412,34 @@ export class Router extends EventEmitter<RouterEvents> {
402
412
  return undefined;
403
413
  }
404
414
 
415
+ /**
416
+ * Merge the render context with the page context.
417
+ *
418
+ * @param page
419
+ * @param ctx
420
+ * @param props
421
+ * @protected
422
+ */
423
+ protected mergeRenderContext(
424
+ page: PageRoute,
425
+ ctx: RouterRenderContext,
426
+ props: Record<string, any>,
427
+ ): void {
428
+ if (page.helmet) {
429
+ const helmet =
430
+ typeof page.helmet === "function" ? page.helmet(props) : page.helmet;
431
+ if (helmet.title) {
432
+ ctx.helmet ??= {};
433
+
434
+ if (ctx.helmet?.title) {
435
+ ctx.helmet.title = `${helmet.title} - ${ctx.helmet.title}`;
436
+ } else {
437
+ ctx.helmet.title = helmet.title;
438
+ }
439
+ }
440
+ }
441
+ }
442
+
405
443
  /**
406
444
  *
407
445
  * @param e
@@ -685,7 +723,7 @@ export interface RouterMatchOptions {
685
723
  /**
686
724
  *
687
725
  */
688
- user?: UserAccountInfo;
726
+ args?: PageContext;
689
727
  }
690
728
 
691
729
  export interface RouterEvents {
@@ -725,6 +763,18 @@ export interface RouterState {
725
763
  *
726
764
  */
727
765
  layers: Array<Layer>;
766
+
767
+ /**
768
+ *
769
+ */
770
+ context: RouterRenderContext;
771
+ }
772
+
773
+ export interface RouterRenderContext {
774
+ /**
775
+ *
776
+ */
777
+ helmet?: RouterRenderHelmetContext;
728
778
  }
729
779
 
730
780
  export interface RouterRenderOptions extends RouterMatchOptions {
@@ -735,8 +785,71 @@ export interface RouterRenderOptions extends RouterMatchOptions {
735
785
  }
736
786
 
737
787
  export interface RouterStackItem {
788
+ /**
789
+ *
790
+ */
738
791
  route: PageRoute;
792
+
793
+ /**
794
+ *
795
+ */
739
796
  config?: Record<string, any>;
797
+
798
+ /**
799
+ *
800
+ */
740
801
  props?: Record<string, any>;
802
+
803
+ /**
804
+ *
805
+ */
741
806
  error?: Error;
742
807
  }
808
+
809
+ export interface RouterRenderHelmetContext {
810
+ /**
811
+ *
812
+ */
813
+ title?: string;
814
+
815
+ /**
816
+ *
817
+ */
818
+ html?: {
819
+ attributes?: Record<string, string>;
820
+ };
821
+
822
+ /**
823
+ *
824
+ */
825
+ body?: {
826
+ attributes?: Record<string, string>;
827
+ };
828
+
829
+ /**
830
+ *
831
+ */
832
+ meta?: Array<{ name: string; content: string }>;
833
+ }
834
+
835
+ export interface RouterRenderResult {
836
+ /**
837
+ *
838
+ */
839
+ element: ReactNode;
840
+
841
+ /**
842
+ *
843
+ */
844
+ layers: Layer[];
845
+
846
+ /**
847
+ *
848
+ */
849
+ redirect?: string;
850
+
851
+ /**
852
+ *
853
+ */
854
+ context: RouterRenderContext;
855
+ }
@@ -1,17 +0,0 @@
1
- import { autoInject, $inject, Alepha } from '@alepha/core';
2
- import { $ as $page, P as PageDescriptorProvider, k as ReactBrowserProvider } from './useRouterState-CvFCmaq7.mjs';
3
- export { N as NestedView, j as RedirectException, R as Router, a as RouterContext, f as RouterHookApi, b as RouterLayerContext, p as pageDescriptorKey, u as useActive, c as useClient, d as useInject, e as useQueryParams, g as useRouter, h as useRouterEvents, i as useRouterState } from './useRouterState-CvFCmaq7.mjs';
4
- import 'react';
5
- import '@alepha/server';
6
- import 'react-dom/client';
7
- import 'path-to-regexp';
8
-
9
- class ReactModule {
10
- alepha = $inject(Alepha);
11
- constructor() {
12
- this.alepha.with(PageDescriptorProvider).with(ReactBrowserProvider);
13
- }
14
- }
15
- autoInject($page, ReactModule);
16
-
17
- export { $page, ReactBrowserProvider, ReactModule };