@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
@@ -2,19 +2,19 @@ import {
2
2
  $env,
3
3
  $hook,
4
4
  $inject,
5
- $logger,
6
5
  Alepha,
7
6
  type Static,
7
+ type TSchema,
8
+ TypeGuard,
8
9
  t,
9
10
  } from "@alepha/core";
10
- import type { ApiLinksResponse } from "@alepha/server";
11
+ import { $logger } from "@alepha/logger";
11
12
  import { createElement, type ReactNode, StrictMode } from "react";
12
13
  import ClientOnly from "../components/ClientOnly.tsx";
13
14
  import ErrorViewer from "../components/ErrorViewer.tsx";
14
15
  import NestedView from "../components/NestedView.tsx";
15
16
  import NotFoundPage from "../components/NotFound.tsx";
16
17
  import { AlephaContext } from "../contexts/AlephaContext.ts";
17
- import { RouterContext } from "../contexts/RouterContext.ts";
18
18
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
19
19
  import {
20
20
  $page,
@@ -23,7 +23,6 @@ import {
23
23
  type PageDescriptorOptions,
24
24
  } from "../descriptors/$page.ts";
25
25
  import { Redirection } from "../errors/Redirection.ts";
26
- import type { HrefLike } from "../hooks/RouterHookApi.ts";
27
26
 
28
27
  const envSchema = t.object({
29
28
  REACT_STRICT_MODE: t.boolean({ default: true }),
@@ -33,7 +32,7 @@ declare module "@alepha/core" {
33
32
  export interface Env extends Partial<Static<typeof envSchema>> {}
34
33
  }
35
34
 
36
- export class PageDescriptorProvider {
35
+ export class ReactPageProvider {
37
36
  protected readonly log = $logger();
38
37
  protected readonly env = $env(envSchema);
39
38
  protected readonly alepha = $inject(Alepha);
@@ -86,29 +85,20 @@ export class PageDescriptorProvider {
86
85
 
87
86
  public url(
88
87
  name: string,
89
- options: { params?: Record<string, string>; base?: string } = {},
88
+ options: { params?: Record<string, string>; host?: string } = {},
90
89
  ): URL {
91
90
  return new URL(
92
91
  this.pathname(name, options),
93
92
  // use provided base or default to http://localhost
94
- options.base ?? `http://localhost`,
93
+ options.host ?? `http://localhost`,
95
94
  );
96
95
  }
97
96
 
98
- public root(state: RouterState, context: PageReactContext): ReactNode {
97
+ public root(state: ReactRouterState): ReactNode {
99
98
  const root = createElement(
100
99
  AlephaContext.Provider,
101
100
  { value: this.alepha },
102
- createElement(
103
- RouterContext.Provider,
104
- {
105
- value: {
106
- state,
107
- context,
108
- },
109
- },
110
- createElement(NestedView, {}, state.layers[0]?.element),
111
- ),
101
+ createElement(NestedView, {}, state.layers[0]?.element),
112
102
  );
113
103
 
114
104
  if (this.env.REACT_STRICT_MODE) {
@@ -118,15 +108,42 @@ export class PageDescriptorProvider {
118
108
  return root;
119
109
  }
120
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
+
135
+ /**
136
+ * Create a new RouterState based on a given route and request.
137
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
138
+ * It also handles errors and redirects.
139
+ */
121
140
  public async createLayers(
122
141
  route: PageRoute,
123
- request: PageRequest,
142
+ state: ReactRouterState,
143
+ previous: PreviousLayerData[] = [],
124
144
  ): Promise<CreateLayersResult> {
125
- const { pathname, search } = request.url;
126
- const layers: Layer[] = []; // result layers
127
145
  let context: Record<string, any> = {}; // all props
128
146
  const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
129
- request.onError = (error) => this.renderError(error); // error handler
130
147
 
131
148
  let parent = route.parent;
132
149
  while (parent) {
@@ -142,8 +159,9 @@ export class PageDescriptorProvider {
142
159
  const config: Record<string, any> = {};
143
160
 
144
161
  try {
162
+ this.convertStringObjectToObject(route.schema?.query, state.query);
145
163
  config.query = route.schema?.query
146
- ? this.alepha.parse(route.schema.query, request.query)
164
+ ? this.alepha.parse(route.schema.query, state.query)
147
165
  : {};
148
166
  } catch (e) {
149
167
  it.error = e as Error;
@@ -152,7 +170,7 @@ export class PageDescriptorProvider {
152
170
 
153
171
  try {
154
172
  config.params = route.schema?.params
155
- ? this.alepha.parse(route.schema.params, request.params)
173
+ ? this.alepha.parse(route.schema.params, state.params)
156
174
  : {};
157
175
  } catch (e) {
158
176
  it.error = e as Error;
@@ -165,7 +183,6 @@ export class PageDescriptorProvider {
165
183
  };
166
184
 
167
185
  // check if previous layer is the same, reuse if possible
168
- const previous = request.previous;
169
186
  if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
170
187
  const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
171
188
 
@@ -203,7 +220,7 @@ export class PageDescriptorProvider {
203
220
  try {
204
221
  const props =
205
222
  (await route.resolve?.({
206
- ...request, // request
223
+ ...state, // request
207
224
  ...config, // params, query
208
225
  ...context, // previous props
209
226
  } as any)) ?? {};
@@ -221,13 +238,12 @@ export class PageDescriptorProvider {
221
238
  } catch (e) {
222
239
  // check if we need to redirect
223
240
  if (e instanceof Redirection) {
224
- return this.createRedirectionLayer(e.page, {
225
- pathname,
226
- search,
227
- });
241
+ return {
242
+ redirect: e.redirect,
243
+ };
228
244
  }
229
245
 
230
- this.log.error(e);
246
+ this.log.error("Page resolver has failed", e);
231
247
 
232
248
  it.error = e as Error;
233
249
  break;
@@ -249,8 +265,8 @@ export class PageDescriptorProvider {
249
265
  const path = acc.replace(/\/+/, "/");
250
266
  const localErrorHandler = this.getErrorHandler(it.route);
251
267
  if (localErrorHandler) {
252
- const onErrorParent = request.onError;
253
- request.onError = (error, context) => {
268
+ const onErrorParent = state.onError;
269
+ state.onError = (error, context) => {
254
270
  const result = localErrorHandler(error, context);
255
271
  // if nothing happen, call the parent
256
272
  if (result === undefined) {
@@ -260,28 +276,51 @@ export class PageDescriptorProvider {
260
276
  };
261
277
  }
262
278
 
279
+ // normal use case
280
+ if (!it.error) {
281
+ try {
282
+ const element = await this.createElement(it.route, {
283
+ ...props,
284
+ ...context,
285
+ });
286
+
287
+ state.layers.push({
288
+ name: it.route.name,
289
+ props,
290
+ part: it.route.path,
291
+ config: it.config,
292
+ element: this.renderView(i + 1, path, element, it.route),
293
+ index: i + 1,
294
+ path,
295
+ route: it.route,
296
+ cache: it.cache,
297
+ });
298
+ } catch (e) {
299
+ it.error = e as Error;
300
+ }
301
+ }
302
+
263
303
  // handler has thrown an error, render an error view
264
304
  if (it.error) {
265
305
  try {
266
306
  let element: ReactNode | Redirection | undefined =
267
- await request.onError(it.error, request);
307
+ await state.onError(it.error, state);
268
308
 
269
309
  if (element === undefined) {
270
310
  throw it.error;
271
311
  }
272
312
 
273
313
  if (element instanceof Redirection) {
274
- return this.createRedirectionLayer(element.page, {
275
- pathname,
276
- search,
277
- });
314
+ return {
315
+ redirect: element.redirect,
316
+ };
278
317
  }
279
318
 
280
319
  if (element === null) {
281
320
  element = this.renderError(it.error);
282
321
  }
283
322
 
284
- layers.push({
323
+ state.layers.push({
285
324
  props,
286
325
  error: it.error,
287
326
  name: it.route.name,
@@ -295,47 +334,21 @@ export class PageDescriptorProvider {
295
334
  break;
296
335
  } catch (e) {
297
336
  if (e instanceof Redirection) {
298
- return this.createRedirectionLayer(e.page, { pathname, search });
337
+ return {
338
+ redirect: e.redirect,
339
+ };
299
340
  }
300
341
  throw e;
301
342
  }
302
343
  }
303
-
304
- // normal use case
305
-
306
- const element = await this.createElement(it.route, {
307
- ...props,
308
- ...context,
309
- });
310
-
311
- layers.push({
312
- name: it.route.name,
313
- props,
314
- part: it.route.path,
315
- config: it.config,
316
- element: this.renderView(i + 1, path, element, it.route),
317
- index: i + 1,
318
- path,
319
- route: it.route,
320
- cache: it.cache,
321
- });
322
344
  }
323
345
 
324
- return { layers, pathname, search };
346
+ return { state };
325
347
  }
326
348
 
327
- protected createRedirectionLayer(
328
- href: HrefLike,
329
- context: {
330
- pathname: string;
331
- search: string;
332
- },
333
- ) {
349
+ protected createRedirectionLayer(redirect: string): CreateLayersResult {
334
350
  return {
335
- layers: [],
336
- redirect: typeof href === "string" ? href : this.href(href),
337
- pathname: context.pathname,
338
- search: context.search,
351
+ redirect,
339
352
  };
340
353
  }
341
354
 
@@ -352,6 +365,12 @@ export class PageDescriptorProvider {
352
365
  page: PageRoute,
353
366
  props: Record<string, any>,
354
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
+
355
374
  if (page.lazy) {
356
375
  const component = await page.lazy(); // load component
357
376
  return createElement(component.default, props);
@@ -601,16 +620,36 @@ export interface AnchorProps {
601
620
  onClick: (ev?: any) => any;
602
621
  }
603
622
 
604
- export interface RouterState {
605
- pathname: string;
606
- search: string;
623
+ export interface ReactRouterState {
624
+ /**
625
+ * Stack of layers for the current page.
626
+ */
607
627
  layers: Array<Layer>;
608
- }
609
628
 
610
- export interface TransitionOptions {
611
- state?: RouterState;
612
- previous?: PreviousLayerData[];
613
- context?: PageReactContext;
629
+ /**
630
+ * URL of the current page.
631
+ */
632
+ url: URL;
633
+
634
+ /**
635
+ * Error handler for the current page.
636
+ */
637
+ onError: ErrorHandler;
638
+
639
+ /**
640
+ * Params extracted from the URL for the current page.
641
+ */
642
+ params: Record<string, any>;
643
+
644
+ /**
645
+ * Query parameters extracted from the URL for the current page.
646
+ */
647
+ query: Record<string, string>;
648
+
649
+ /**
650
+ * Optional meta information associated with the current page.
651
+ */
652
+ meta: Record<string, any>;
614
653
  }
615
654
 
616
655
  export interface RouterStackItem {
@@ -621,33 +660,11 @@ export interface RouterStackItem {
621
660
  cache?: boolean;
622
661
  }
623
662
 
624
- export interface RouterRenderResult {
625
- state: RouterState;
626
- context: PageReactContext;
627
- redirect?: string;
628
- }
629
-
630
- export interface PageRequest extends PageReactContext {
631
- params: Record<string, any>;
632
- query: Record<string, string>;
633
-
634
- // previous layers (browser history or browser hydration, always null on server)
663
+ export interface TransitionOptions {
635
664
  previous?: PreviousLayerData[];
636
665
  }
637
666
 
638
- export interface CreateLayersResult extends RouterState {
667
+ export interface CreateLayersResult {
639
668
  redirect?: string;
640
- }
641
-
642
- /**
643
- * It's like RouterState, but publicly available in React context.
644
- * This is where we store all plugin data!
645
- */
646
- export interface PageReactContext {
647
- url: URL;
648
- onError: ErrorHandler;
649
- links?: ApiLinksResponse;
650
-
651
- params: Record<string, any>;
652
- query: Record<string, string>;
669
+ state?: ReactRouterState;
653
670
  }