@alepha/react 0.9.4 → 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.
@@ -1,4 +1,13 @@
1
- import { $env, $hook, $inject, Alepha, type Static, t } from "@alepha/core";
1
+ import {
2
+ $env,
3
+ $hook,
4
+ $inject,
5
+ Alepha,
6
+ type Static,
7
+ type TSchema,
8
+ TypeGuard,
9
+ t,
10
+ } from "@alepha/core";
2
11
  import { $logger } from "@alepha/logger";
3
12
  import { createElement, type ReactNode, StrictMode } from "react";
4
13
  import ClientOnly from "../components/ClientOnly.tsx";
@@ -99,6 +108,30 @@ export class ReactPageProvider {
99
108
  return root;
100
109
  }
101
110
 
111
+ protected convertStringObjectToObject = (
112
+ schema?: TSchema,
113
+ value?: any,
114
+ ): any => {
115
+ if (TypeGuard.IsObject(schema) && typeof value === "object") {
116
+ for (const key in schema.properties) {
117
+ if (
118
+ TypeGuard.IsObject(schema.properties[key]) &&
119
+ typeof value[key] === "string"
120
+ ) {
121
+ try {
122
+ value[key] = this.alepha.parse(
123
+ schema.properties[key],
124
+ decodeURIComponent(value[key]),
125
+ );
126
+ } catch (e) {
127
+ // ignore
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return value;
133
+ };
134
+
102
135
  /**
103
136
  * Create a new RouterState based on a given route and request.
104
137
  * This method resolves the layers for the route, applying any query and params schemas defined in the route.
@@ -126,6 +159,7 @@ export class ReactPageProvider {
126
159
  const config: Record<string, any> = {};
127
160
 
128
161
  try {
162
+ this.convertStringObjectToObject(route.schema?.query, state.query);
129
163
  config.query = route.schema?.query
130
164
  ? this.alepha.parse(route.schema.query, state.query)
131
165
  : {};
@@ -331,6 +365,12 @@ export class ReactPageProvider {
331
365
  page: PageRoute,
332
366
  props: Record<string, any>,
333
367
  ): Promise<ReactNode> {
368
+ if (page.lazy && page.component) {
369
+ this.log.warn(
370
+ `Page ${page.name} has both lazy and component options, lazy will be used`,
371
+ );
372
+ }
373
+
334
374
  if (page.lazy) {
335
375
  const component = await page.lazy(); // load component
336
376
  return createElement(component.default, props);
@@ -605,6 +645,11 @@ export interface ReactRouterState {
605
645
  * Query parameters extracted from the URL for the current page.
606
646
  */
607
647
  query: Record<string, string>;
648
+
649
+ /**
650
+ * Optional meta information associated with the current page.
651
+ */
652
+ meta: Record<string, any>;
608
653
  }
609
654
 
610
655
  export interface RouterStackItem {
@@ -12,6 +12,7 @@ import {
12
12
  import { $logger } from "@alepha/logger";
13
13
  import {
14
14
  type ServerHandler,
15
+ ServerProvider,
15
16
  ServerRouterProvider,
16
17
  ServerTimingProvider,
17
18
  } from "@alepha/server";
@@ -21,6 +22,7 @@ import { renderToString } from "react-dom/server";
21
22
  import {
22
23
  $page,
23
24
  type PageDescriptorRenderOptions,
25
+ type PageDescriptorRenderResult,
24
26
  } from "../descriptors/$page.ts";
25
27
  import { Redirection } from "../errors/Redirection.ts";
26
28
  import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
@@ -53,6 +55,7 @@ export class ReactServerProvider {
53
55
  protected readonly log = $logger();
54
56
  protected readonly alepha = $inject(Alepha);
55
57
  protected readonly pageApi = $inject(ReactPageProvider);
58
+ protected readonly serverProvider = $inject(ServerProvider);
56
59
  protected readonly serverStaticProvider = $inject(ServerStaticProvider);
57
60
  protected readonly serverRouterProvider = $inject(ServerRouterProvider);
58
61
  protected readonly serverTimingProvider = $inject(ServerTimingProvider);
@@ -74,6 +77,19 @@ export class ReactServerProvider {
74
77
 
75
78
  for (const page of pages) {
76
79
  page.render = this.createRenderFunction(page.name);
80
+ page.fetch = async (options) => {
81
+ const response = await fetch(
82
+ `${this.serverProvider.hostname}/${page.pathname(options)}`,
83
+ );
84
+ const html = await response.text();
85
+ if (options?.html) return { html, response };
86
+ // take only text inside the root div
87
+ const match = html.match(this.ROOT_DIV_REGEX);
88
+ if (match) {
89
+ return { html: match[3], response };
90
+ }
91
+ throw new AlephaError("Invalid HTML response");
92
+ };
77
93
  }
78
94
 
79
95
  // development mode
@@ -194,7 +210,9 @@ export class ReactServerProvider {
194
210
  * For testing purposes, creates a render function that can be used.
195
211
  */
196
212
  protected createRenderFunction(name: string, withIndex = false) {
197
- return async (options: PageDescriptorRenderOptions = {}) => {
213
+ return async (
214
+ options: PageDescriptorRenderOptions = {},
215
+ ): Promise<PageDescriptorRenderResult> => {
198
216
  const page = this.pageApi.page(name);
199
217
  const url = new URL(this.pageApi.url(name, options));
200
218
 
@@ -204,6 +222,7 @@ export class ReactServerProvider {
204
222
  query: options.query ?? {},
205
223
  onError: () => null,
206
224
  layers: [],
225
+ meta: {},
207
226
  };
208
227
 
209
228
  const state = entry as ReactRouterState;
@@ -222,7 +241,7 @@ export class ReactServerProvider {
222
241
  );
223
242
 
224
243
  if (redirect) {
225
- throw new AlephaError("Redirection is not supported in this context");
244
+ return { state, html: "", redirect };
226
245
  }
227
246
 
228
247
  if (!withIndex && !options.html) {
@@ -241,7 +260,7 @@ export class ReactServerProvider {
241
260
  );
242
261
 
243
262
  if (html instanceof Redirection) {
244
- throw new Error("Redirection is not supported in this context");
263
+ return { state, html: "", redirect };
245
264
  }
246
265
 
247
266
  const result = {
@@ -33,8 +33,8 @@ export class ReactRouter<T extends object> {
33
33
  public path(
34
34
  name: keyof VirtualRouter<T>,
35
35
  config: {
36
- params?: Record<string, string>;
37
- query?: Record<string, string>;
36
+ params?: Record<string, any>;
37
+ query?: Record<string, any>;
38
38
  } = {},
39
39
  ): string {
40
40
  return this.pageApi.pathname(name as string, {
@@ -116,17 +116,14 @@ export class ReactRouter<T extends object> {
116
116
  await this.browser?.go(path as string, options);
117
117
  }
118
118
 
119
- public anchor(
120
- path: string,
121
- options?: { params?: Record<string, any> },
122
- ): AnchorProps;
119
+ public anchor(path: string, options?: RouterGoOptions): AnchorProps;
123
120
  public anchor(
124
121
  path: keyof VirtualRouter<T>,
125
- options?: { params?: Record<string, any> },
122
+ options?: RouterGoOptions,
126
123
  ): AnchorProps;
127
124
  public anchor(
128
125
  path: string | keyof VirtualRouter<T>,
129
- options: { params?: Record<string, any> } = {},
126
+ options: RouterGoOptions = {},
130
127
  ): AnchorProps {
131
128
  let href = path as string;
132
129