@alepha/react 0.7.3 → 0.7.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.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { t, $logger, $inject, Alepha, $hook, OPTIONS, __bind } from '@alepha/core';
2
- import { ServerRouterProvider, ServerTimingProvider, ServerLinksProvider, apiLinksResponseSchema, ServerModule } from '@alepha/server';
3
- import { ServerCacheModule } from '@alepha/server-cache';
4
- import { P as PageDescriptorProvider, $ as $page } from './useRouterState-D5__ZcUV.js';
5
- export { C as ClientOnly, E as ErrorBoundary, L as Link, N as NestedView, l as ReactBrowserProvider, R as RedirectionError, a as RouterContext, c as RouterHookApi, b as RouterLayerContext, k as isPageRoute, u as useActive, d as useAlepha, e as useClient, f as useInject, g as useQueryParams, h as useRouter, i as useRouterEvents, j as useRouterState } from './useRouterState-D5__ZcUV.js';
2
+ import { ServerRouterProvider, ServerTimingProvider, ServerLinksProvider, apiLinksResponseSchema, HttpClient, AlephaServer } from '@alepha/server';
3
+ import { AlephaServerCache } from '@alepha/server-cache';
4
+ import { P as PageDescriptorProvider, $ as $page, R as RouterContext, a as RouterLayerContext, b as ReactBrowserProvider, u as useRouterEvents } from './ReactBrowserProvider-ufHSOTmv.js';
5
+ export { C as ClientOnly, E as ErrorBoundary, N as NestedView, c as RedirectionError, i as isPageRoute, d as useAlepha } from './ReactBrowserProvider-ufHSOTmv.js';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { ServerStaticProvider } from '@alepha/server-static';
9
9
  import { renderToString } from 'react-dom/server';
10
- import 'react/jsx-runtime';
11
- import 'react';
10
+ import { jsx } from 'react/jsx-runtime';
11
+ import React, { useContext, useMemo, useState, useEffect } from 'react';
12
12
  import '@alepha/router';
13
13
 
14
14
  class ServerHeadProvider {
@@ -353,12 +353,239 @@ class ReactServerProvider {
353
353
  }
354
354
  }
355
355
 
356
- class ReactModule {
357
- alepha = $inject(Alepha);
358
- constructor() {
359
- this.alepha.with(ServerModule).with(ServerCacheModule).with(ServerLinksProvider).with(PageDescriptorProvider).with(ReactServerProvider);
356
+ class RouterHookApi {
357
+ constructor(pages, state, layer, browser) {
358
+ this.pages = pages;
359
+ this.state = state;
360
+ this.layer = layer;
361
+ this.browser = browser;
362
+ }
363
+ get current() {
364
+ return this.state;
365
+ }
366
+ get pathname() {
367
+ return this.state.pathname;
368
+ }
369
+ get query() {
370
+ const query = {};
371
+ for (const [key, value] of new URLSearchParams(
372
+ this.state.search
373
+ ).entries()) {
374
+ query[key] = String(value);
375
+ }
376
+ return query;
377
+ }
378
+ async back() {
379
+ this.browser?.history.back();
380
+ }
381
+ async forward() {
382
+ this.browser?.history.forward();
383
+ }
384
+ async invalidate(props) {
385
+ await this.browser?.invalidate(props);
386
+ }
387
+ /**
388
+ * Create a valid href for the given pathname.
389
+ *
390
+ * @param pathname
391
+ * @param layer
392
+ */
393
+ createHref(pathname, layer = this.layer, options = {}) {
394
+ if (typeof pathname === "object") {
395
+ pathname = pathname.options.path ?? "";
396
+ }
397
+ if (options.params) {
398
+ for (const [key, value] of Object.entries(options.params)) {
399
+ pathname = pathname.replace(`:${key}`, String(value));
400
+ }
401
+ }
402
+ return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
403
+ }
404
+ async go(path, options) {
405
+ for (const page of this.pages) {
406
+ if (page.name === path) {
407
+ path = page.path ?? "";
408
+ break;
409
+ }
410
+ }
411
+ await this.browser?.go(this.createHref(path, this.layer, options), options);
412
+ }
413
+ anchor(path, options = {}) {
414
+ for (const page of this.pages) {
415
+ if (page.name === path) {
416
+ path = page.path ?? "";
417
+ break;
418
+ }
419
+ }
420
+ const href = this.createHref(path, this.layer, options);
421
+ return {
422
+ href,
423
+ onClick: (ev) => {
424
+ ev.stopPropagation();
425
+ ev.preventDefault();
426
+ this.go(path, options).catch(console.error);
427
+ }
428
+ };
429
+ }
430
+ /**
431
+ * Set query params.
432
+ *
433
+ * @param record
434
+ * @param options
435
+ */
436
+ setQueryParams(record, options = {}) {
437
+ const func = typeof record === "function" ? record : () => record;
438
+ const search = new URLSearchParams(func(this.query)).toString();
439
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
440
+ if (options.push) {
441
+ window.history.pushState({}, "", state);
442
+ } else {
443
+ window.history.replaceState({}, "", state);
444
+ }
360
445
  }
361
446
  }
362
- __bind($page, ReactModule);
363
447
 
364
- export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };
448
+ const useRouter = () => {
449
+ const ctx = useContext(RouterContext);
450
+ const layer = useContext(RouterLayerContext);
451
+ if (!ctx || !layer) {
452
+ throw new Error("useRouter must be used within a RouterProvider");
453
+ }
454
+ const pages = useMemo(() => {
455
+ return ctx.alepha.get(PageDescriptorProvider).getPages();
456
+ }, []);
457
+ return useMemo(
458
+ () => new RouterHookApi(
459
+ pages,
460
+ ctx.state,
461
+ layer,
462
+ ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
463
+ ),
464
+ [layer]
465
+ );
466
+ };
467
+
468
+ const Link = (props) => {
469
+ React.useContext(RouterContext);
470
+ const router = useRouter();
471
+ const to = typeof props.to === "string" ? props.to : props.to[OPTIONS].path;
472
+ if (!to) {
473
+ return null;
474
+ }
475
+ const can = typeof props.to === "string" ? void 0 : props.to[OPTIONS].can;
476
+ if (can && !can()) {
477
+ return null;
478
+ }
479
+ const name = typeof props.to === "string" ? void 0 : props.to[OPTIONS].name;
480
+ const anchorProps = {
481
+ ...props,
482
+ to: void 0
483
+ };
484
+ return /* @__PURE__ */ jsx("a", { ...router.anchor(to), ...anchorProps, children: props.children ?? name });
485
+ };
486
+
487
+ const useActive = (path) => {
488
+ const router = useRouter();
489
+ const ctx = useContext(RouterContext);
490
+ const layer = useContext(RouterLayerContext);
491
+ if (!ctx || !layer) {
492
+ throw new Error("useRouter must be used within a RouterProvider");
493
+ }
494
+ let name;
495
+ if (typeof path === "object" && path.options.name) {
496
+ name = path.options.name;
497
+ }
498
+ const [current, setCurrent] = useState(ctx.state.pathname);
499
+ const href = useMemo(() => router.createHref(path, layer), [path, layer]);
500
+ const [isPending, setPending] = useState(false);
501
+ const isActive = current === href;
502
+ useRouterEvents({
503
+ onEnd: ({ state }) => setCurrent(state.pathname)
504
+ });
505
+ return {
506
+ name,
507
+ isPending,
508
+ isActive,
509
+ anchorProps: {
510
+ href,
511
+ onClick: (ev) => {
512
+ ev.stopPropagation();
513
+ ev.preventDefault();
514
+ if (isActive) return;
515
+ if (isPending) return;
516
+ setPending(true);
517
+ router.go(href).then(() => {
518
+ setPending(false);
519
+ });
520
+ }
521
+ }
522
+ };
523
+ };
524
+
525
+ const useInject = (clazz) => {
526
+ const ctx = useContext(RouterContext);
527
+ if (!ctx) {
528
+ throw new Error("useRouter must be used within a <RouterProvider>");
529
+ }
530
+ return useMemo(() => ctx.alepha.get(clazz), []);
531
+ };
532
+
533
+ const useClient = (_scope) => {
534
+ return useInject(HttpClient).of();
535
+ };
536
+
537
+ const useQueryParams = (schema, options = {}) => {
538
+ const ctx = useContext(RouterContext);
539
+ if (!ctx) {
540
+ throw new Error("useQueryParams must be used within a RouterProvider");
541
+ }
542
+ const key = options.key ?? "q";
543
+ const router = useRouter();
544
+ const querystring = router.query[key];
545
+ const [queryParams, setQueryParams] = useState(
546
+ decode(ctx.alepha, schema, router.query[key])
547
+ );
548
+ useEffect(() => {
549
+ setQueryParams(decode(ctx.alepha, schema, querystring));
550
+ }, [querystring]);
551
+ return [
552
+ queryParams,
553
+ (queryParams2) => {
554
+ setQueryParams(queryParams2);
555
+ router.setQueryParams((data) => {
556
+ return { ...data, [key]: encode(ctx.alepha, schema, queryParams2) };
557
+ });
558
+ }
559
+ ];
560
+ };
561
+ const encode = (alepha, schema, data) => {
562
+ return btoa(JSON.stringify(alepha.parse(schema, data)));
563
+ };
564
+ const decode = (alepha, schema, data) => {
565
+ try {
566
+ return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
567
+ } catch (_error) {
568
+ return {};
569
+ }
570
+ };
571
+
572
+ const useRouterState = () => {
573
+ const ctx = useContext(RouterContext);
574
+ const layer = useContext(RouterLayerContext);
575
+ if (!ctx || !layer) {
576
+ throw new Error("useRouter must be used within a RouterProvider");
577
+ }
578
+ const [state, setState] = useState(ctx.state);
579
+ useRouterEvents({
580
+ onEnd: ({ state: state2 }) => setState({ ...state2 })
581
+ });
582
+ return state;
583
+ };
584
+
585
+ class AlephaReact {
586
+ name = "alepha.react";
587
+ $services = (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(ReactServerProvider).with(PageDescriptorProvider);
588
+ }
589
+ __bind($page, AlephaReact);
590
+
591
+ export { $page, AlephaReact, Link, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RouterContext, RouterHookApi, RouterLayerContext, useActive, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -13,11 +13,11 @@
13
13
  "src"
14
14
  ],
15
15
  "dependencies": {
16
- "@alepha/core": "0.7.3",
17
- "@alepha/router": "0.7.3",
18
- "@alepha/server": "0.7.3",
19
- "@alepha/server-cache": "0.7.3",
20
- "@alepha/server-static": "0.7.3",
16
+ "@alepha/core": "0.7.4",
17
+ "@alepha/router": "0.7.4",
18
+ "@alepha/server": "0.7.4",
19
+ "@alepha/server-cache": "0.7.4",
20
+ "@alepha/server-static": "0.7.4",
21
21
  "react-dom": "^19.1.0"
22
22
  },
23
23
  "devDependencies": {
@@ -1,23 +1,26 @@
1
- import { __bind, $inject, Alepha } from "@alepha/core";
1
+ import { __bind, type Alepha, type Module } from "@alepha/core";
2
2
  import { $page } from "./descriptors/$page.ts";
3
3
  import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
4
4
  import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
5
5
  import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
6
6
  import { ReactBrowserRenderer } from "./providers/ReactBrowserRenderer.ts";
7
7
 
8
- export * from "./index.shared";
8
+ // ---------------------------------------------------------------------------------------------------------------------
9
+
10
+ export * from "./providers/BrowserRouterProvider.ts";
11
+ export * from "./providers/PageDescriptorProvider.ts";
9
12
  export * from "./providers/ReactBrowserProvider.ts";
10
13
 
11
- export class ReactModule {
12
- protected readonly alepha = $inject(Alepha);
14
+ // ---------------------------------------------------------------------------------------------------------------------
13
15
 
14
- constructor() {
15
- this.alepha //
16
+ export class AlephaReact implements Module {
17
+ public readonly name = "alepha.react";
18
+ public readonly $services = (alepha: Alepha) =>
19
+ alepha
16
20
  .with(PageDescriptorProvider)
17
21
  .with(ReactBrowserProvider)
18
22
  .with(BrowserRouterProvider)
19
23
  .with(ReactBrowserRenderer);
20
- }
21
24
  }
22
25
 
23
- __bind($page, ReactModule);
26
+ __bind($page, AlephaReact);
package/src/index.ts CHANGED
@@ -1,10 +1,6 @@
1
- import { __bind, $inject, Alepha } from "@alepha/core";
2
- import {
3
- ServerLinksProvider,
4
- ServerModule,
5
- type ServerRequest,
6
- } from "@alepha/server";
7
- import { ServerCacheModule } from "@alepha/server-cache";
1
+ import { __bind, type Alepha, type Module } from "@alepha/core";
2
+ import { AlephaServer, type ServerRequest } from "@alepha/server";
3
+ import { AlephaServerCache } from "@alepha/server-cache";
8
4
  import { $page } from "./descriptors/$page.ts";
9
5
  import {
10
6
  PageDescriptorProvider,
@@ -15,6 +11,8 @@ import {
15
11
  import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
16
12
  import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
17
13
 
14
+ // ---------------------------------------------------------------------------------------------------------------------
15
+
18
16
  export { default as NestedView } from "./components/NestedView.tsx";
19
17
  export * from "./errors/RedirectionError.ts";
20
18
  export * from "./index.shared.ts";
@@ -22,6 +20,8 @@ export * from "./providers/PageDescriptorProvider.ts";
22
20
  export * from "./providers/ReactBrowserProvider.ts";
23
21
  export * from "./providers/ReactServerProvider.ts";
24
22
 
23
+ // ---------------------------------------------------------------------------------------------------------------------
24
+
25
25
  declare module "@alepha/core" {
26
26
  interface Hooks {
27
27
  "react:browser:render": {
@@ -52,17 +52,25 @@ declare module "@alepha/core" {
52
52
  }
53
53
  }
54
54
 
55
- export class ReactModule {
56
- protected readonly alepha = $inject(Alepha);
55
+ // ---------------------------------------------------------------------------------------------------------------------
57
56
 
58
- constructor() {
59
- this.alepha //
60
- .with(ServerModule)
61
- .with(ServerCacheModule)
62
- .with(ServerLinksProvider)
63
- .with(PageDescriptorProvider)
64
- .with(ReactServerProvider);
65
- }
57
+ /**
58
+ * Alepha React Module
59
+ *
60
+ * Alepha React Module contains a router for client-side navigation and server-side rendering.
61
+ * Routes can be defined using the `$page` descriptor.
62
+ *
63
+ * @see {@link $page}
64
+ * @module alepha.react
65
+ */
66
+ export class AlephaReact implements Module {
67
+ public readonly name = "alepha.react";
68
+ public readonly $services = (alepha: Alepha) =>
69
+ alepha
70
+ .with(AlephaServer)
71
+ .with(AlephaServerCache)
72
+ .with(ReactServerProvider)
73
+ .with(PageDescriptorProvider);
66
74
  }
67
75
 
68
- __bind($page, ReactModule);
76
+ __bind($page, AlephaReact);
@@ -17,6 +17,7 @@ declare module "@alepha/core" {
17
17
  interface Env extends Partial<Static<typeof envSchema>> {}
18
18
  }
19
19
 
20
+ // TODO: move to ReactBrowserProvider when it will be removed from server-side imports
20
21
  export class ReactBrowserRenderer {
21
22
  protected readonly browserProvider = $inject(ReactBrowserProvider);
22
23
  protected readonly browserRouterProvider = $inject(BrowserRouterProvider);
@@ -29,7 +29,7 @@ import {
29
29
  import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
30
30
  import { ServerHeadProvider } from "./ServerHeadProvider.ts";
31
31
 
32
- export const envSchema = t.object({
32
+ const envSchema = t.object({
33
33
  REACT_SERVER_DIST: t.string({ default: "public" }),
34
34
  REACT_SERVER_PREFIX: t.string({ default: "" }),
35
35
  REACT_SSR_ENABLED: t.optional(t.boolean()),