@alepha/react 0.9.2 → 0.9.4

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 (40) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +378 -325
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +570 -458
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +305 -213
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +304 -212
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +567 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +16 -13
  13. package/src/components/ErrorViewer.tsx +1 -1
  14. package/src/components/Link.tsx +4 -24
  15. package/src/components/NestedView.tsx +20 -9
  16. package/src/components/NotFound.tsx +5 -2
  17. package/src/descriptors/$page.ts +86 -12
  18. package/src/errors/Redirection.ts +13 -0
  19. package/src/hooks/useActive.ts +28 -30
  20. package/src/hooks/useAlepha.ts +16 -2
  21. package/src/hooks/useClient.ts +7 -2
  22. package/src/hooks/useInject.ts +4 -1
  23. package/src/hooks/useQueryParams.ts +9 -6
  24. package/src/hooks/useRouter.ts +18 -30
  25. package/src/hooks/useRouterEvents.ts +7 -4
  26. package/src/hooks/useRouterState.ts +8 -20
  27. package/src/hooks/useSchema.ts +10 -15
  28. package/src/hooks/useStore.ts +9 -8
  29. package/src/index.browser.ts +11 -11
  30. package/src/index.shared.ts +4 -5
  31. package/src/index.ts +21 -30
  32. package/src/providers/ReactBrowserProvider.ts +155 -65
  33. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  34. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
  35. package/src/providers/ReactServerProvider.ts +100 -68
  36. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
  37. package/src/contexts/RouterContext.ts +0 -14
  38. package/src/errors/RedirectionError.ts +0 -10
  39. package/src/providers/BrowserRouterProvider.ts +0 -146
  40. package/src/providers/ReactBrowserRenderer.ts +0 -93
@@ -1,27 +1,19 @@
1
- import {
2
- $env,
3
- $hook,
4
- $inject,
5
- $logger,
6
- Alepha,
7
- type Static,
8
- t,
9
- } from "@alepha/core";
10
- import type { ApiLinksResponse } from "@alepha/server";
1
+ import { $env, $hook, $inject, Alepha, type Static, t } from "@alepha/core";
2
+ import { $logger } from "@alepha/logger";
11
3
  import { createElement, type ReactNode, StrictMode } from "react";
12
4
  import ClientOnly from "../components/ClientOnly.tsx";
13
5
  import ErrorViewer from "../components/ErrorViewer.tsx";
14
6
  import NestedView from "../components/NestedView.tsx";
15
7
  import NotFoundPage from "../components/NotFound.tsx";
16
8
  import { AlephaContext } from "../contexts/AlephaContext.ts";
17
- import { RouterContext } from "../contexts/RouterContext.ts";
18
9
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
19
10
  import {
20
11
  $page,
12
+ type ErrorHandler,
21
13
  type PageDescriptor,
22
14
  type PageDescriptorOptions,
23
15
  } from "../descriptors/$page.ts";
24
- import { RedirectionError } from "../errors/RedirectionError.ts";
16
+ import { Redirection } from "../errors/Redirection.ts";
25
17
 
26
18
  const envSchema = t.object({
27
19
  REACT_STRICT_MODE: t.boolean({ default: true }),
@@ -31,7 +23,7 @@ declare module "@alepha/core" {
31
23
  export interface Env extends Partial<Static<typeof envSchema>> {}
32
24
  }
33
25
 
34
- export class PageDescriptorProvider {
26
+ export class ReactPageProvider {
35
27
  protected readonly log = $logger();
36
28
  protected readonly env = $env(envSchema);
37
29
  protected readonly alepha = $inject(Alepha);
@@ -51,10 +43,13 @@ export class PageDescriptorProvider {
51
43
  throw new Error(`Page ${name} not found`);
52
44
  }
53
45
 
54
- public url(
46
+ public pathname(
55
47
  name: string,
56
- options: { params?: Record<string, string>; base?: string } = {},
57
- ): URL {
48
+ options: {
49
+ params?: Record<string, string>;
50
+ query?: Record<string, string>;
51
+ } = {},
52
+ ) {
58
53
  const page = this.page(name);
59
54
  if (!page) {
60
55
  throw new Error(`Page ${name} not found`);
@@ -69,26 +64,32 @@ export class PageDescriptorProvider {
69
64
 
70
65
  url = this.compile(url, options.params ?? {});
71
66
 
67
+ if (options.query) {
68
+ const query = new URLSearchParams(options.query);
69
+ if (query.toString()) {
70
+ url += `?${query.toString()}`;
71
+ }
72
+ }
73
+
74
+ return url.replace(/\/\/+/g, "/") || "/";
75
+ }
76
+
77
+ public url(
78
+ name: string,
79
+ options: { params?: Record<string, string>; host?: string } = {},
80
+ ): URL {
72
81
  return new URL(
73
- url.replace(/\/\/+/g, "/") || "/",
74
- options.base ?? `http://localhost`,
82
+ this.pathname(name, options),
83
+ // use provided base or default to http://localhost
84
+ options.host ?? `http://localhost`,
75
85
  );
76
86
  }
77
87
 
78
- public root(state: RouterState, context: PageReactContext): ReactNode {
88
+ public root(state: ReactRouterState): ReactNode {
79
89
  const root = createElement(
80
90
  AlephaContext.Provider,
81
91
  { value: this.alepha },
82
- createElement(
83
- RouterContext.Provider,
84
- {
85
- value: {
86
- state,
87
- context,
88
- },
89
- },
90
- createElement(NestedView, {}, state.layers[0]?.element),
91
- ),
92
+ createElement(NestedView, {}, state.layers[0]?.element),
92
93
  );
93
94
 
94
95
  if (this.env.REACT_STRICT_MODE) {
@@ -98,15 +99,18 @@ export class PageDescriptorProvider {
98
99
  return root;
99
100
  }
100
101
 
102
+ /**
103
+ * Create a new RouterState based on a given route and request.
104
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
105
+ * It also handles errors and redirects.
106
+ */
101
107
  public async createLayers(
102
108
  route: PageRoute,
103
- request: PageRequest,
109
+ state: ReactRouterState,
110
+ previous: PreviousLayerData[] = [],
104
111
  ): Promise<CreateLayersResult> {
105
- const { pathname, search } = request.url;
106
- const layers: Layer[] = []; // result layers
107
112
  let context: Record<string, any> = {}; // all props
108
113
  const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
109
- request.onError = (error) => this.renderError(error); // error handler
110
114
 
111
115
  let parent = route.parent;
112
116
  while (parent) {
@@ -123,7 +127,7 @@ export class PageDescriptorProvider {
123
127
 
124
128
  try {
125
129
  config.query = route.schema?.query
126
- ? this.alepha.parse(route.schema.query, request.query)
130
+ ? this.alepha.parse(route.schema.query, state.query)
127
131
  : {};
128
132
  } catch (e) {
129
133
  it.error = e as Error;
@@ -132,7 +136,7 @@ export class PageDescriptorProvider {
132
136
 
133
137
  try {
134
138
  config.params = route.schema?.params
135
- ? this.alepha.parse(route.schema.params, request.params)
139
+ ? this.alepha.parse(route.schema.params, state.params)
136
140
  : {};
137
141
  } catch (e) {
138
142
  it.error = e as Error;
@@ -145,7 +149,6 @@ export class PageDescriptorProvider {
145
149
  };
146
150
 
147
151
  // check if previous layer is the same, reuse if possible
148
- const previous = request.previous;
149
152
  if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
150
153
  const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
151
154
 
@@ -183,7 +186,7 @@ export class PageDescriptorProvider {
183
186
  try {
184
187
  const props =
185
188
  (await route.resolve?.({
186
- ...request, // request
189
+ ...state, // request
187
190
  ...config, // params, query
188
191
  ...context, // previous props
189
192
  } as any)) ?? {};
@@ -200,16 +203,13 @@ export class PageDescriptorProvider {
200
203
  };
201
204
  } catch (e) {
202
205
  // check if we need to redirect
203
- if (e instanceof RedirectionError) {
206
+ if (e instanceof Redirection) {
204
207
  return {
205
- layers: [],
206
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
207
- pathname,
208
- search,
208
+ redirect: e.redirect,
209
209
  };
210
210
  }
211
211
 
212
- this.log.error(e);
212
+ this.log.error("Page resolver has failed", e);
213
213
 
214
214
  it.error = e as Error;
215
215
  break;
@@ -231,54 +231,94 @@ export class PageDescriptorProvider {
231
231
  const path = acc.replace(/\/+/, "/");
232
232
  const localErrorHandler = this.getErrorHandler(it.route);
233
233
  if (localErrorHandler) {
234
- request.onError = localErrorHandler;
234
+ const onErrorParent = state.onError;
235
+ state.onError = (error, context) => {
236
+ const result = localErrorHandler(error, context);
237
+ // if nothing happen, call the parent
238
+ if (result === undefined) {
239
+ return onErrorParent(error, context);
240
+ }
241
+ return result;
242
+ };
243
+ }
244
+
245
+ // normal use case
246
+ if (!it.error) {
247
+ try {
248
+ const element = await this.createElement(it.route, {
249
+ ...props,
250
+ ...context,
251
+ });
252
+
253
+ state.layers.push({
254
+ name: it.route.name,
255
+ props,
256
+ part: it.route.path,
257
+ config: it.config,
258
+ element: this.renderView(i + 1, path, element, it.route),
259
+ index: i + 1,
260
+ path,
261
+ route: it.route,
262
+ cache: it.cache,
263
+ });
264
+ } catch (e) {
265
+ it.error = e as Error;
266
+ }
235
267
  }
236
268
 
237
269
  // handler has thrown an error, render an error view
238
270
  if (it.error) {
239
- let element: ReactNode = await request.onError(it.error);
240
- if (element === null) {
241
- element = this.renderError(it.error);
242
- }
271
+ try {
272
+ let element: ReactNode | Redirection | undefined =
273
+ await state.onError(it.error, state);
243
274
 
244
- layers.push({
245
- props,
246
- error: it.error,
247
- name: it.route.name,
248
- part: it.route.path,
249
- config: it.config,
250
- element: this.renderView(i + 1, path, element, it.route),
251
- index: i + 1,
252
- path,
253
- route: it.route,
254
- });
255
- break;
256
- }
275
+ if (element === undefined) {
276
+ throw it.error;
277
+ }
257
278
 
258
- // normal use case
279
+ if (element instanceof Redirection) {
280
+ return {
281
+ redirect: element.redirect,
282
+ };
283
+ }
284
+
285
+ if (element === null) {
286
+ element = this.renderError(it.error);
287
+ }
259
288
 
260
- const element = await this.createElement(it.route, {
261
- ...props,
262
- ...context,
263
- });
264
-
265
- layers.push({
266
- name: it.route.name,
267
- props,
268
- part: it.route.path,
269
- config: it.config,
270
- element: this.renderView(i + 1, path, element, it.route),
271
- index: i + 1,
272
- path,
273
- route: it.route,
274
- cache: it.cache,
275
- });
289
+ state.layers.push({
290
+ props,
291
+ error: it.error,
292
+ name: it.route.name,
293
+ part: it.route.path,
294
+ config: it.config,
295
+ element: this.renderView(i + 1, path, element, it.route),
296
+ index: i + 1,
297
+ path,
298
+ route: it.route,
299
+ });
300
+ break;
301
+ } catch (e) {
302
+ if (e instanceof Redirection) {
303
+ return {
304
+ redirect: e.redirect,
305
+ };
306
+ }
307
+ throw e;
308
+ }
309
+ }
276
310
  }
277
311
 
278
- return { layers, pathname, search };
312
+ return { state };
313
+ }
314
+
315
+ protected createRedirectionLayer(redirect: string): CreateLayersResult {
316
+ return {
317
+ redirect,
318
+ };
279
319
  }
280
320
 
281
- protected getErrorHandler(route: PageRoute) {
321
+ protected getErrorHandler(route: PageRoute): ErrorHandler | undefined {
282
322
  if (route.errorHandler) return route.errorHandler;
283
323
  let parent = route.parent;
284
324
  while (parent) {
@@ -374,6 +414,10 @@ export class PageDescriptorProvider {
374
414
  const pages = this.alepha.descriptors($page);
375
415
 
376
416
  const hasParent = (it: PageDescriptor) => {
417
+ if (it.options.parent) {
418
+ return true;
419
+ }
420
+
377
421
  for (const page of pages) {
378
422
  const children = page.options.children
379
423
  ? Array.isArray(page.options.children)
@@ -406,7 +450,7 @@ export class PageDescriptorProvider {
406
450
  name: "notFound",
407
451
  cache: true,
408
452
  component: NotFoundPage,
409
- afterHandler: ({ reply }) => {
453
+ onServerResponse: ({ reply }) => {
410
454
  reply.status = 404;
411
455
  },
412
456
  });
@@ -424,6 +468,18 @@ export class PageDescriptorProvider {
424
468
  : target.options.children()
425
469
  : [];
426
470
 
471
+ const getChildrenFromParent = (it: PageDescriptor): PageDescriptor[] => {
472
+ const children = [];
473
+ for (const page of pages) {
474
+ if (page.options.parent === it) {
475
+ children.push(page);
476
+ }
477
+ }
478
+ return children;
479
+ };
480
+
481
+ children.push(...getChildrenFromParent(target));
482
+
427
483
  return {
428
484
  ...target.options,
429
485
  name: target.name,
@@ -521,19 +577,34 @@ export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
521
577
 
522
578
  export interface AnchorProps {
523
579
  href: string;
524
- onClick: (ev: any) => any;
580
+ onClick: (ev?: any) => any;
525
581
  }
526
582
 
527
- export interface RouterState {
528
- pathname: string;
529
- search: string;
583
+ export interface ReactRouterState {
584
+ /**
585
+ * Stack of layers for the current page.
586
+ */
530
587
  layers: Array<Layer>;
531
- }
532
588
 
533
- export interface TransitionOptions {
534
- state?: RouterState;
535
- previous?: PreviousLayerData[];
536
- context?: PageReactContext;
589
+ /**
590
+ * URL of the current page.
591
+ */
592
+ url: URL;
593
+
594
+ /**
595
+ * Error handler for the current page.
596
+ */
597
+ onError: ErrorHandler;
598
+
599
+ /**
600
+ * Params extracted from the URL for the current page.
601
+ */
602
+ params: Record<string, any>;
603
+
604
+ /**
605
+ * Query parameters extracted from the URL for the current page.
606
+ */
607
+ query: Record<string, string>;
537
608
  }
538
609
 
539
610
  export interface RouterStackItem {
@@ -544,30 +615,11 @@ export interface RouterStackItem {
544
615
  cache?: boolean;
545
616
  }
546
617
 
547
- export interface RouterRenderResult {
548
- state: RouterState;
549
- context: PageReactContext;
550
- redirect?: string;
551
- }
552
-
553
- export interface PageRequest extends PageReactContext {
554
- params: Record<string, any>;
555
- query: Record<string, string>;
556
-
557
- // previous layers (browser history or browser hydration, always null on server)
618
+ export interface TransitionOptions {
558
619
  previous?: PreviousLayerData[];
559
620
  }
560
621
 
561
- export interface CreateLayersResult extends RouterState {
622
+ export interface CreateLayersResult {
562
623
  redirect?: string;
563
- }
564
-
565
- /**
566
- * It's like RouterState, but publicly available in React context.
567
- * This is where we store all plugin data!
568
- */
569
- export interface PageReactContext {
570
- url: URL;
571
- onError: (error: Error) => ReactNode;
572
- links?: ApiLinksResponse;
624
+ state?: ReactRouterState;
573
625
  }