@alepha/react 0.7.1 → 0.7.3

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,17 +1,54 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@alepha/core');
4
- var useRouterState = require('./useRouterState-AdK-XeM2.cjs');
4
+ var useRouterState = require('./useRouterState-C2uo0jXu.cjs');
5
+ var client = require('react-dom/client');
5
6
  require('react/jsx-runtime');
6
7
  require('react');
7
8
  require('@alepha/server');
8
- require('react-dom/client');
9
9
  require('@alepha/router');
10
10
 
11
+ const envSchema = core.t.object({
12
+ REACT_ROOT_ID: core.t.string({ default: "root" })
13
+ });
14
+ class ReactBrowserRenderer {
15
+ browserProvider = core.$inject(useRouterState.ReactBrowserProvider);
16
+ browserRouterProvider = core.$inject(useRouterState.BrowserRouterProvider);
17
+ env = core.$inject(envSchema);
18
+ log = core.$logger();
19
+ root;
20
+ getRootElement() {
21
+ const root = this.browserProvider.document.getElementById(
22
+ this.env.REACT_ROOT_ID
23
+ );
24
+ if (root) {
25
+ return root;
26
+ }
27
+ const div = this.browserProvider.document.createElement("div");
28
+ div.id = this.env.REACT_ROOT_ID;
29
+ this.browserProvider.document.body.prepend(div);
30
+ return div;
31
+ }
32
+ ready = core.$hook({
33
+ name: "react:browser:render",
34
+ handler: async ({ state, context, hydration }) => {
35
+ const element = this.browserRouterProvider.root(state, context);
36
+ if (hydration?.layers) {
37
+ this.root = client.hydrateRoot(this.getRootElement(), element);
38
+ this.log.info("Hydrated root element");
39
+ } else {
40
+ this.root ??= client.createRoot(this.getRootElement());
41
+ this.root.render(element);
42
+ this.log.info("Created root element");
43
+ }
44
+ }
45
+ });
46
+ }
47
+
11
48
  class ReactModule {
12
49
  alepha = core.$inject(core.Alepha);
13
50
  constructor() {
14
- this.alepha.with(useRouterState.PageDescriptorProvider).with(useRouterState.ReactBrowserProvider).with(useRouterState.BrowserRouterProvider);
51
+ this.alepha.with(useRouterState.PageDescriptorProvider).with(useRouterState.ReactBrowserProvider).with(useRouterState.BrowserRouterProvider).with(ReactBrowserRenderer);
15
52
  }
16
53
  }
17
54
  core.__bind(useRouterState.$page, ReactModule);
@@ -1,16 +1,53 @@
1
- import { __bind, $inject, Alepha } from '@alepha/core';
2
- import { $ as $page, P as PageDescriptorProvider, l as ReactBrowserProvider, B as BrowserRouterProvider } from './useRouterState-qoMq7Y9J.js';
3
- export { C as ClientOnly, E as ErrorBoundary, L as Link, N as NestedView, R as RedirectionError, a as RouterContext, c as RouterHookApi, b as RouterLayerContext, 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-qoMq7Y9J.js';
1
+ import { t, $inject, $logger, $hook, __bind, Alepha } from '@alepha/core';
2
+ import { l as ReactBrowserProvider, B as BrowserRouterProvider, $ as $page, P as PageDescriptorProvider } from './useRouterState-D5__ZcUV.js';
3
+ export { C as ClientOnly, E as ErrorBoundary, L as Link, N as NestedView, R as RedirectionError, a as RouterContext, c as RouterHookApi, b as RouterLayerContext, 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';
4
+ import { hydrateRoot, createRoot } from 'react-dom/client';
4
5
  import 'react/jsx-runtime';
5
6
  import 'react';
6
7
  import '@alepha/server';
7
- import 'react-dom/client';
8
8
  import '@alepha/router';
9
9
 
10
+ const envSchema = t.object({
11
+ REACT_ROOT_ID: t.string({ default: "root" })
12
+ });
13
+ class ReactBrowserRenderer {
14
+ browserProvider = $inject(ReactBrowserProvider);
15
+ browserRouterProvider = $inject(BrowserRouterProvider);
16
+ env = $inject(envSchema);
17
+ log = $logger();
18
+ root;
19
+ getRootElement() {
20
+ const root = this.browserProvider.document.getElementById(
21
+ this.env.REACT_ROOT_ID
22
+ );
23
+ if (root) {
24
+ return root;
25
+ }
26
+ const div = this.browserProvider.document.createElement("div");
27
+ div.id = this.env.REACT_ROOT_ID;
28
+ this.browserProvider.document.body.prepend(div);
29
+ return div;
30
+ }
31
+ ready = $hook({
32
+ name: "react:browser:render",
33
+ handler: async ({ state, context, hydration }) => {
34
+ const element = this.browserRouterProvider.root(state, context);
35
+ if (hydration?.layers) {
36
+ this.root = hydrateRoot(this.getRootElement(), element);
37
+ this.log.info("Hydrated root element");
38
+ } else {
39
+ this.root ??= createRoot(this.getRootElement());
40
+ this.root.render(element);
41
+ this.log.info("Created root element");
42
+ }
43
+ }
44
+ });
45
+ }
46
+
10
47
  class ReactModule {
11
48
  alepha = $inject(Alepha);
12
49
  constructor() {
13
- this.alepha.with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider);
50
+ this.alepha.with(PageDescriptorProvider).with(ReactBrowserProvider).with(BrowserRouterProvider).with(ReactBrowserRenderer);
14
51
  }
15
52
  }
16
53
  __bind($page, ReactModule);
package/dist/index.cjs CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  var core = require('@alepha/core');
4
4
  var server = require('@alepha/server');
5
- var useRouterState = require('./useRouterState-AdK-XeM2.cjs');
5
+ var serverCache = require('@alepha/server-cache');
6
+ var useRouterState = require('./useRouterState-C2uo0jXu.cjs');
6
7
  var node_fs = require('node:fs');
7
8
  var node_path = require('node:path');
8
9
  var serverStatic = require('@alepha/server-static');
9
10
  var server$1 = require('react-dom/server');
10
11
  require('react/jsx-runtime');
11
12
  require('react');
12
- require('react-dom/client');
13
13
  require('@alepha/router');
14
14
 
15
15
  class ServerHeadProvider {
@@ -138,7 +138,6 @@ class ReactServerProvider {
138
138
  return;
139
139
  }
140
140
  reply.headers["content-type"] = "text/html";
141
- reply.status = 200;
142
141
  return this.template;
143
142
  }
144
143
  });
@@ -278,7 +277,6 @@ class ReactServerProvider {
278
277
  if (state.redirect) {
279
278
  return reply.redirect(state.redirect);
280
279
  }
281
- reply.status = 200;
282
280
  reply.headers["content-type"] = "text/html";
283
281
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
284
282
  reply.headers.pragma = "no-cache";
@@ -286,7 +284,9 @@ class ReactServerProvider {
286
284
  if (page.cache && serverRequest.user) {
287
285
  delete context.links;
288
286
  }
289
- return this.renderToHtml(template, state, context);
287
+ const html = this.renderToHtml(template, state, context);
288
+ page.afterHandler?.(serverRequest);
289
+ return html;
290
290
  };
291
291
  }
292
292
  renderToHtml(template, state, context) {
@@ -357,7 +357,7 @@ class ReactServerProvider {
357
357
  class ReactModule {
358
358
  alepha = core.$inject(core.Alepha);
359
359
  constructor() {
360
- this.alepha.with(server.ServerModule).with(server.ServerLinksProvider).with(useRouterState.PageDescriptorProvider).with(ReactServerProvider);
360
+ this.alepha.with(server.ServerModule).with(serverCache.ServerCacheModule).with(server.ServerLinksProvider).with(useRouterState.PageDescriptorProvider).with(ReactServerProvider);
361
361
  }
362
362
  }
363
363
  core.__bind(useRouterState.$page, ReactModule);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _alepha_core from '@alepha/core';
2
2
  import { TSchema as TSchema$1, KIND, OPTIONS, Static as Static$1, Async, Alepha, Service, TObject as TObject$1 } from '@alepha/core';
3
- import { ServerRoute, ApiLinksResponse, HttpClient, ClientScope, HttpVirtualClient, ServerRouterProvider, ServerTimingProvider, ServerHandler, ServerRequest } from '@alepha/server';
3
+ import { ServerRoute, ServerRequest, ApiLinksResponse, HttpClient, ClientScope, HttpVirtualClient, ServerRouterProvider, ServerTimingProvider, ServerHandler } from '@alepha/server';
4
4
  import * as React from 'react';
5
5
  import React__default, { PropsWithChildren, ReactNode, FC, ErrorInfo, AnchorHTMLAttributes } from 'react';
6
6
  import { Root } from 'react-dom/client';
@@ -227,6 +227,7 @@ interface PageDescriptorOptions<TConfig extends PageConfigSchema = PageConfigSch
227
227
  * If true, the page will be rendered on the client-side.
228
228
  */
229
229
  client?: boolean | ClientOnlyProps;
230
+ afterHandler?: (request: ServerRequest) => any;
230
231
  }
231
232
  interface PageDescriptor<TConfig extends PageConfigSchema = PageConfigSchema, TProps extends object = TPropsDefault, TPropsParent extends object = TPropsParentDefault> {
232
233
  [KIND]: typeof KEY;
@@ -296,11 +297,11 @@ interface PageRequestConfig<TConfig extends PageConfigSchema = PageConfigSchema>
296
297
  }
297
298
  type PageResolve<TConfig extends PageConfigSchema = PageConfigSchema, TPropsParent extends object = TPropsParentDefault> = PageRequestConfig<TConfig> & TPropsParent & PageReactContext;
298
299
 
299
- declare const envSchema$2: _alepha_core.TObject<{
300
+ declare const envSchema$1: _alepha_core.TObject<{
300
301
  REACT_STRICT_MODE: TBoolean;
301
302
  }>;
302
303
  declare module "@alepha/core" {
303
- interface Env extends Partial<Static$1<typeof envSchema$2>> {
304
+ interface Env extends Partial<Static$1<typeof envSchema$1>> {
304
305
  }
305
306
  }
306
307
  declare class PageDescriptorProvider {
@@ -445,22 +446,12 @@ declare class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
445
446
  root(state: RouterState, context: PageReactContext): ReactNode;
446
447
  }
447
448
 
448
- declare const envSchema$1: _alepha_core.TObject<{
449
- REACT_ROOT_ID: TString;
450
- }>;
451
- declare module "@alepha/core" {
452
- interface Env extends Partial<Static$1<typeof envSchema$1>> {
453
- }
454
- }
455
449
  declare class ReactBrowserProvider {
456
450
  protected readonly log: _alepha_core.Logger;
457
451
  protected readonly client: HttpClient;
458
452
  protected readonly alepha: Alepha;
459
453
  protected readonly router: BrowserRouterProvider;
460
454
  protected readonly headProvider: BrowserHeadProvider;
461
- protected readonly env: {
462
- REACT_ROOT_ID: string;
463
- };
464
455
  protected root: Root;
465
456
  transitioning?: {
466
457
  to: string;
@@ -470,11 +461,6 @@ declare class ReactBrowserProvider {
470
461
  get history(): History;
471
462
  get url(): string;
472
463
  invalidate(props?: Record<string, any>): Promise<void>;
473
- /**
474
- *
475
- * @param url
476
- * @param options
477
- */
478
464
  go(url: string, options?: RouterGoOptions): Promise<void>;
479
465
  protected render(options?: {
480
466
  url?: string;
@@ -482,19 +468,8 @@ declare class ReactBrowserProvider {
482
468
  }): Promise<RouterRenderResult>;
483
469
  /**
484
470
  * Get embedded layers from the server.
485
- *
486
- * @protected
487
471
  */
488
472
  protected getHydrationState(): ReactHydrationState | undefined;
489
- /**
490
- *
491
- * @protected
492
- */
493
- protected getRootElement(): HTMLElement;
494
- /**
495
- *
496
- * @protected
497
- */
498
473
  readonly ready: _alepha_core.HookDescriptor<"ready">;
499
474
  readonly onTransitionEnd: _alepha_core.HookDescriptor<"react:transition:end">;
500
475
  }
@@ -542,30 +517,11 @@ declare class RouterHookApi {
542
517
  constructor(pages: PageRoute[], state: RouterState, layer: {
543
518
  path: string;
544
519
  }, browser?: ReactBrowserProvider | undefined);
545
- /**
546
- *
547
- */
548
520
  get current(): RouterState;
549
- /**
550
- *
551
- */
552
521
  get pathname(): string;
553
- /**
554
- *
555
- */
556
522
  get query(): Record<string, string>;
557
- /**
558
- *
559
- */
560
523
  back(): Promise<void>;
561
- /**
562
- *
563
- */
564
524
  forward(): Promise<void>;
565
- /**
566
- *
567
- * @param props
568
- */
569
525
  invalidate(props?: Record<string, any>): Promise<void>;
570
526
  /**
571
527
  * Create a valid href for the given pathname.
@@ -734,9 +690,9 @@ declare class ReactServerProvider {
734
690
  protected readonly serverTimingProvider: ServerTimingProvider;
735
691
  protected readonly env: {
736
692
  REACT_SSR_ENABLED?: boolean | undefined;
737
- REACT_ROOT_ID: string;
738
693
  REACT_SERVER_DIST: string;
739
694
  REACT_SERVER_PREFIX: string;
695
+ REACT_ROOT_ID: string;
740
696
  };
741
697
  protected readonly ROOT_DIV_REGEX: RegExp;
742
698
  readonly onConfigure: _alepha_core.HookDescriptor<"configure">;
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { t, $logger, $inject, Alepha, $hook, OPTIONS, __bind } from '@alepha/core';
2
2
  import { ServerRouterProvider, ServerTimingProvider, ServerLinksProvider, apiLinksResponseSchema, ServerModule } from '@alepha/server';
3
- import { P as PageDescriptorProvider, $ as $page } from './useRouterState-qoMq7Y9J.js';
4
- 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-qoMq7Y9J.js';
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';
5
6
  import { existsSync } from 'node:fs';
6
7
  import { join } from 'node:path';
7
8
  import { ServerStaticProvider } from '@alepha/server-static';
8
9
  import { renderToString } from 'react-dom/server';
9
10
  import 'react/jsx-runtime';
10
11
  import 'react';
11
- import 'react-dom/client';
12
12
  import '@alepha/router';
13
13
 
14
14
  class ServerHeadProvider {
@@ -137,7 +137,6 @@ class ReactServerProvider {
137
137
  return;
138
138
  }
139
139
  reply.headers["content-type"] = "text/html";
140
- reply.status = 200;
141
140
  return this.template;
142
141
  }
143
142
  });
@@ -277,7 +276,6 @@ class ReactServerProvider {
277
276
  if (state.redirect) {
278
277
  return reply.redirect(state.redirect);
279
278
  }
280
- reply.status = 200;
281
279
  reply.headers["content-type"] = "text/html";
282
280
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
283
281
  reply.headers.pragma = "no-cache";
@@ -285,7 +283,9 @@ class ReactServerProvider {
285
283
  if (page.cache && serverRequest.user) {
286
284
  delete context.links;
287
285
  }
288
- return this.renderToHtml(template, state, context);
286
+ const html = this.renderToHtml(template, state, context);
287
+ page.afterHandler?.(serverRequest);
288
+ return html;
289
289
  };
290
290
  }
291
291
  renderToHtml(template, state, context) {
@@ -356,7 +356,7 @@ class ReactServerProvider {
356
356
  class ReactModule {
357
357
  alepha = $inject(Alepha);
358
358
  constructor() {
359
- this.alepha.with(ServerModule).with(ServerLinksProvider).with(PageDescriptorProvider).with(ReactServerProvider);
359
+ this.alepha.with(ServerModule).with(ServerCacheModule).with(ServerLinksProvider).with(PageDescriptorProvider).with(ReactServerProvider);
360
360
  }
361
361
  }
362
362
  __bind($page, ReactModule);
@@ -4,7 +4,6 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var core = require('@alepha/core');
5
5
  var React = require('react');
6
6
  var server = require('@alepha/server');
7
- var client = require('react-dom/client');
8
7
  var router = require('@alepha/router');
9
8
 
10
9
  const KEY = "PAGE";
@@ -279,6 +278,39 @@ const NestedView = (props) => {
279
278
  return /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary, { fallback: app.context.onError, children: element });
280
279
  };
281
280
 
281
+ function NotFoundPage() {
282
+ return /* @__PURE__ */ jsxRuntime.jsxs(
283
+ "div",
284
+ {
285
+ style: {
286
+ height: "100vh",
287
+ display: "flex",
288
+ flexDirection: "column",
289
+ justifyContent: "center",
290
+ alignItems: "center",
291
+ textAlign: "center",
292
+ fontFamily: "sans-serif",
293
+ padding: "1rem"
294
+ },
295
+ children: [
296
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1rem", marginBottom: "0.5rem" }, children: "This page does not exist" }),
297
+ /* @__PURE__ */ jsxRuntime.jsx(
298
+ "a",
299
+ {
300
+ href: "/",
301
+ style: {
302
+ fontSize: "0.7rem",
303
+ color: "#007bff",
304
+ textDecoration: "none"
305
+ },
306
+ children: "\u2190 Back to home"
307
+ }
308
+ )
309
+ ]
310
+ }
311
+ );
312
+ }
313
+
282
314
  class RedirectionError extends Error {
283
315
  page;
284
316
  constructor(page) {
@@ -287,12 +319,12 @@ class RedirectionError extends Error {
287
319
  }
288
320
  }
289
321
 
290
- const envSchema$1 = core.t.object({
322
+ const envSchema = core.t.object({
291
323
  REACT_STRICT_MODE: core.t.boolean({ default: true })
292
324
  });
293
325
  class PageDescriptorProvider {
294
326
  log = core.$logger();
295
- env = core.$inject(envSchema$1);
327
+ env = core.$inject(envSchema);
296
328
  alepha = core.$inject(core.Alepha);
297
329
  pages = [];
298
330
  getPages() {
@@ -576,6 +608,7 @@ class PageDescriptorProvider {
576
608
  configure = core.$hook({
577
609
  name: "configure",
578
610
  handler: () => {
611
+ let hasNotFoundHandler = false;
579
612
  const pages = this.alepha.getDescriptorValues($page);
580
613
  for (const { value, key } of pages) {
581
614
  value[core.OPTIONS].name ??= key;
@@ -584,8 +617,22 @@ class PageDescriptorProvider {
584
617
  if (value[core.OPTIONS].parent) {
585
618
  continue;
586
619
  }
620
+ if (value[core.OPTIONS].path === "/*") {
621
+ hasNotFoundHandler = true;
622
+ }
587
623
  this.add(this.map(pages, value));
588
624
  }
625
+ if (!hasNotFoundHandler && pages.length > 0) {
626
+ this.add({
627
+ path: "/*",
628
+ name: "notFound",
629
+ cache: true,
630
+ component: NotFoundPage,
631
+ afterHandler: ({ reply }) => {
632
+ reply.status = 404;
633
+ }
634
+ });
635
+ }
589
636
  }
590
637
  });
591
638
  map(pages, target) {
@@ -738,7 +785,7 @@ class BrowserRouterProvider extends router.RouterProvider {
738
785
  if (state.layers.length === 0) {
739
786
  state.layers.push({
740
787
  name: "not-found",
741
- element: "Not Found",
788
+ element: React.createElement(NotFoundPage),
742
789
  index: 0,
743
790
  path: "/"
744
791
  });
@@ -779,16 +826,12 @@ class BrowserRouterProvider extends router.RouterProvider {
779
826
  }
780
827
  }
781
828
 
782
- const envSchema = core.t.object({
783
- REACT_ROOT_ID: core.t.string({ default: "root" })
784
- });
785
829
  class ReactBrowserProvider {
786
830
  log = core.$logger();
787
831
  client = core.$inject(server.HttpClient);
788
832
  alepha = core.$inject(core.Alepha);
789
833
  router = core.$inject(BrowserRouterProvider);
790
834
  headProvider = core.$inject(BrowserHeadProvider);
791
- env = core.$inject(envSchema);
792
835
  root;
793
836
  transitioning;
794
837
  state = {
@@ -826,11 +869,6 @@ class ReactBrowserProvider {
826
869
  }
827
870
  await this.render({ previous });
828
871
  }
829
- /**
830
- *
831
- * @param url
832
- * @param options
833
- */
834
872
  async go(url, options = {}) {
835
873
  const result = await this.render({
836
874
  url
@@ -864,8 +902,6 @@ class ReactBrowserProvider {
864
902
  }
865
903
  /**
866
904
  * Get embedded layers from the server.
867
- *
868
- * @protected
869
905
  */
870
906
  getHydrationState() {
871
907
  try {
@@ -876,25 +912,7 @@ class ReactBrowserProvider {
876
912
  console.error(error);
877
913
  }
878
914
  }
879
- /**
880
- *
881
- * @protected
882
- */
883
- getRootElement() {
884
- const root = this.document.getElementById(this.env.REACT_ROOT_ID);
885
- if (root) {
886
- return root;
887
- }
888
- const div = this.document.createElement("div");
889
- div.id = this.env.REACT_ROOT_ID;
890
- this.document.body.prepend(div);
891
- return div;
892
- }
893
915
  // -------------------------------------------------------------------------------------------------------------------
894
- /**
895
- *
896
- * @protected
897
- */
898
916
  ready = core.$hook({
899
917
  name: "ready",
900
918
  handler: async () => {
@@ -914,15 +932,6 @@ class ReactBrowserProvider {
914
932
  context,
915
933
  hydration
916
934
  });
917
- const element = this.router.root(this.state, context);
918
- if (previous.length > 0) {
919
- this.root = client.hydrateRoot(this.getRootElement(), element);
920
- this.log.info("Hydrated root element");
921
- } else {
922
- this.root ??= client.createRoot(this.getRootElement());
923
- this.root.render(element);
924
- this.log.info("Created root element");
925
- }
926
935
  window.addEventListener("popstate", () => {
927
936
  this.render();
928
937
  });
@@ -943,21 +952,12 @@ class RouterHookApi {
943
952
  this.layer = layer;
944
953
  this.browser = browser;
945
954
  }
946
- /**
947
- *
948
- */
949
955
  get current() {
950
956
  return this.state;
951
957
  }
952
- /**
953
- *
954
- */
955
958
  get pathname() {
956
959
  return this.state.pathname;
957
960
  }
958
- /**
959
- *
960
- */
961
961
  get query() {
962
962
  const query = {};
963
963
  for (const [key, value] of new URLSearchParams(
@@ -967,22 +967,12 @@ class RouterHookApi {
967
967
  }
968
968
  return query;
969
969
  }
970
- /**
971
- *
972
- */
973
970
  async back() {
974
971
  this.browser?.history.back();
975
972
  }
976
- /**
977
- *
978
- */
979
973
  async forward() {
980
974
  this.browser?.history.forward();
981
975
  }
982
- /**
983
- *
984
- * @param props
985
- */
986
976
  async invalidate(props) {
987
977
  await this.browser?.invalidate(props);
988
978
  }
@@ -2,7 +2,6 @@ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { __descriptor, OPTIONS, NotImplementedError, KIND, t, $logger, $inject, Alepha, $hook } from '@alepha/core';
3
3
  import React, { useState, useEffect, createContext, useContext, createElement, StrictMode, useMemo } from 'react';
4
4
  import { HttpClient } from '@alepha/server';
5
- import { hydrateRoot, createRoot } from 'react-dom/client';
6
5
  import { RouterProvider } from '@alepha/router';
7
6
 
8
7
  const KEY = "PAGE";
@@ -277,6 +276,39 @@ const NestedView = (props) => {
277
276
  return /* @__PURE__ */ jsx(ErrorBoundary, { fallback: app.context.onError, children: element });
278
277
  };
279
278
 
279
+ function NotFoundPage() {
280
+ return /* @__PURE__ */ jsxs(
281
+ "div",
282
+ {
283
+ style: {
284
+ height: "100vh",
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ justifyContent: "center",
288
+ alignItems: "center",
289
+ textAlign: "center",
290
+ fontFamily: "sans-serif",
291
+ padding: "1rem"
292
+ },
293
+ children: [
294
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1rem", marginBottom: "0.5rem" }, children: "This page does not exist" }),
295
+ /* @__PURE__ */ jsx(
296
+ "a",
297
+ {
298
+ href: "/",
299
+ style: {
300
+ fontSize: "0.7rem",
301
+ color: "#007bff",
302
+ textDecoration: "none"
303
+ },
304
+ children: "\u2190 Back to home"
305
+ }
306
+ )
307
+ ]
308
+ }
309
+ );
310
+ }
311
+
280
312
  class RedirectionError extends Error {
281
313
  page;
282
314
  constructor(page) {
@@ -285,12 +317,12 @@ class RedirectionError extends Error {
285
317
  }
286
318
  }
287
319
 
288
- const envSchema$1 = t.object({
320
+ const envSchema = t.object({
289
321
  REACT_STRICT_MODE: t.boolean({ default: true })
290
322
  });
291
323
  class PageDescriptorProvider {
292
324
  log = $logger();
293
- env = $inject(envSchema$1);
325
+ env = $inject(envSchema);
294
326
  alepha = $inject(Alepha);
295
327
  pages = [];
296
328
  getPages() {
@@ -574,6 +606,7 @@ class PageDescriptorProvider {
574
606
  configure = $hook({
575
607
  name: "configure",
576
608
  handler: () => {
609
+ let hasNotFoundHandler = false;
577
610
  const pages = this.alepha.getDescriptorValues($page);
578
611
  for (const { value, key } of pages) {
579
612
  value[OPTIONS].name ??= key;
@@ -582,8 +615,22 @@ class PageDescriptorProvider {
582
615
  if (value[OPTIONS].parent) {
583
616
  continue;
584
617
  }
618
+ if (value[OPTIONS].path === "/*") {
619
+ hasNotFoundHandler = true;
620
+ }
585
621
  this.add(this.map(pages, value));
586
622
  }
623
+ if (!hasNotFoundHandler && pages.length > 0) {
624
+ this.add({
625
+ path: "/*",
626
+ name: "notFound",
627
+ cache: true,
628
+ component: NotFoundPage,
629
+ afterHandler: ({ reply }) => {
630
+ reply.status = 404;
631
+ }
632
+ });
633
+ }
587
634
  }
588
635
  });
589
636
  map(pages, target) {
@@ -736,7 +783,7 @@ class BrowserRouterProvider extends RouterProvider {
736
783
  if (state.layers.length === 0) {
737
784
  state.layers.push({
738
785
  name: "not-found",
739
- element: "Not Found",
786
+ element: createElement(NotFoundPage),
740
787
  index: 0,
741
788
  path: "/"
742
789
  });
@@ -777,16 +824,12 @@ class BrowserRouterProvider extends RouterProvider {
777
824
  }
778
825
  }
779
826
 
780
- const envSchema = t.object({
781
- REACT_ROOT_ID: t.string({ default: "root" })
782
- });
783
827
  class ReactBrowserProvider {
784
828
  log = $logger();
785
829
  client = $inject(HttpClient);
786
830
  alepha = $inject(Alepha);
787
831
  router = $inject(BrowserRouterProvider);
788
832
  headProvider = $inject(BrowserHeadProvider);
789
- env = $inject(envSchema);
790
833
  root;
791
834
  transitioning;
792
835
  state = {
@@ -824,11 +867,6 @@ class ReactBrowserProvider {
824
867
  }
825
868
  await this.render({ previous });
826
869
  }
827
- /**
828
- *
829
- * @param url
830
- * @param options
831
- */
832
870
  async go(url, options = {}) {
833
871
  const result = await this.render({
834
872
  url
@@ -862,8 +900,6 @@ class ReactBrowserProvider {
862
900
  }
863
901
  /**
864
902
  * Get embedded layers from the server.
865
- *
866
- * @protected
867
903
  */
868
904
  getHydrationState() {
869
905
  try {
@@ -874,25 +910,7 @@ class ReactBrowserProvider {
874
910
  console.error(error);
875
911
  }
876
912
  }
877
- /**
878
- *
879
- * @protected
880
- */
881
- getRootElement() {
882
- const root = this.document.getElementById(this.env.REACT_ROOT_ID);
883
- if (root) {
884
- return root;
885
- }
886
- const div = this.document.createElement("div");
887
- div.id = this.env.REACT_ROOT_ID;
888
- this.document.body.prepend(div);
889
- return div;
890
- }
891
913
  // -------------------------------------------------------------------------------------------------------------------
892
- /**
893
- *
894
- * @protected
895
- */
896
914
  ready = $hook({
897
915
  name: "ready",
898
916
  handler: async () => {
@@ -912,15 +930,6 @@ class ReactBrowserProvider {
912
930
  context,
913
931
  hydration
914
932
  });
915
- const element = this.router.root(this.state, context);
916
- if (previous.length > 0) {
917
- this.root = hydrateRoot(this.getRootElement(), element);
918
- this.log.info("Hydrated root element");
919
- } else {
920
- this.root ??= createRoot(this.getRootElement());
921
- this.root.render(element);
922
- this.log.info("Created root element");
923
- }
924
933
  window.addEventListener("popstate", () => {
925
934
  this.render();
926
935
  });
@@ -941,21 +950,12 @@ class RouterHookApi {
941
950
  this.layer = layer;
942
951
  this.browser = browser;
943
952
  }
944
- /**
945
- *
946
- */
947
953
  get current() {
948
954
  return this.state;
949
955
  }
950
- /**
951
- *
952
- */
953
956
  get pathname() {
954
957
  return this.state.pathname;
955
958
  }
956
- /**
957
- *
958
- */
959
959
  get query() {
960
960
  const query = {};
961
961
  for (const [key, value] of new URLSearchParams(
@@ -965,22 +965,12 @@ class RouterHookApi {
965
965
  }
966
966
  return query;
967
967
  }
968
- /**
969
- *
970
- */
971
968
  async back() {
972
969
  this.browser?.history.back();
973
970
  }
974
- /**
975
- *
976
- */
977
971
  async forward() {
978
972
  this.browser?.history.forward();
979
973
  }
980
- /**
981
- *
982
- * @param props
983
- */
984
974
  async invalidate(props) {
985
975
  await this.browser?.invalidate(props);
986
976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -13,10 +13,11 @@
13
13
  "src"
14
14
  ],
15
15
  "dependencies": {
16
- "@alepha/core": "0.7.1",
17
- "@alepha/router": "0.7.1",
18
- "@alepha/server": "0.7.1",
19
- "@alepha/server-static": "0.7.1",
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",
20
21
  "react-dom": "^19.1.0"
21
22
  },
22
23
  "devDependencies": {
@@ -0,0 +1,30 @@
1
+ export default function NotFoundPage() {
2
+ return (
3
+ <div
4
+ style={{
5
+ height: "100vh",
6
+ display: "flex",
7
+ flexDirection: "column",
8
+ justifyContent: "center",
9
+ alignItems: "center",
10
+ textAlign: "center",
11
+ fontFamily: "sans-serif",
12
+ padding: "1rem",
13
+ }}
14
+ >
15
+ <h1 style={{ fontSize: "1rem", marginBottom: "0.5rem" }}>
16
+ This page does not exist
17
+ </h1>
18
+ <a
19
+ href="/"
20
+ style={{
21
+ fontSize: "0.7rem",
22
+ color: "#007bff",
23
+ textDecoration: "none",
24
+ }}
25
+ >
26
+ ← Back to home
27
+ </a>
28
+ </div>
29
+ );
30
+ }
@@ -7,7 +7,7 @@ import {
7
7
  type Static,
8
8
  type TSchema,
9
9
  } from "@alepha/core";
10
- import type { ServerRoute } from "@alepha/server";
10
+ import type { ServerRequest, ServerRoute } from "@alepha/server";
11
11
  import type { FC, ReactNode } from "react";
12
12
  import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
13
13
  import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
@@ -115,6 +115,8 @@ export interface PageDescriptorOptions<
115
115
  * If true, the page will be rendered on the client-side.
116
116
  */
117
117
  client?: boolean | ClientOnlyProps;
118
+
119
+ afterHandler?: (request: ServerRequest) => any;
118
120
  }
119
121
 
120
122
  export interface PageDescriptor<
@@ -19,23 +19,14 @@ export class RouterHookApi {
19
19
  private readonly browser?: ReactBrowserProvider,
20
20
  ) {}
21
21
 
22
- /**
23
- *
24
- */
25
22
  public get current(): RouterState {
26
23
  return this.state;
27
24
  }
28
25
 
29
- /**
30
- *
31
- */
32
26
  public get pathname(): string {
33
27
  return this.state.pathname;
34
28
  }
35
29
 
36
- /**
37
- *
38
- */
39
30
  public get query(): Record<string, string> {
40
31
  const query: Record<string, string> = {};
41
32
 
@@ -48,24 +39,14 @@ export class RouterHookApi {
48
39
  return query;
49
40
  }
50
41
 
51
- /**
52
- *
53
- */
54
42
  public async back() {
55
43
  this.browser?.history.back();
56
44
  }
57
45
 
58
- /**
59
- *
60
- */
61
46
  public async forward() {
62
47
  this.browser?.history.forward();
63
48
  }
64
49
 
65
- /**
66
- *
67
- * @param props
68
- */
69
50
  public async invalidate(props?: Record<string, any>) {
70
51
  await this.browser?.invalidate(props);
71
52
  }
@@ -3,6 +3,7 @@ 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
+ import { ReactBrowserRenderer } from "./providers/ReactBrowserRenderer.ts";
6
7
 
7
8
  export * from "./index.shared";
8
9
  export * from "./providers/ReactBrowserProvider.ts";
@@ -14,7 +15,8 @@ export class ReactModule {
14
15
  this.alepha //
15
16
  .with(PageDescriptorProvider)
16
17
  .with(ReactBrowserProvider)
17
- .with(BrowserRouterProvider);
18
+ .with(BrowserRouterProvider)
19
+ .with(ReactBrowserRenderer);
18
20
  }
19
21
  }
20
22
 
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  ServerModule,
5
5
  type ServerRequest,
6
6
  } from "@alepha/server";
7
+ import { ServerCacheModule } from "@alepha/server-cache";
7
8
  import { $page } from "./descriptors/$page.ts";
8
9
  import {
9
10
  PageDescriptorProvider,
@@ -57,6 +58,7 @@ export class ReactModule {
57
58
  constructor() {
58
59
  this.alepha //
59
60
  .with(ServerModule)
61
+ .with(ServerCacheModule)
60
62
  .with(ServerLinksProvider)
61
63
  .with(PageDescriptorProvider)
62
64
  .with(ReactServerProvider);
@@ -1,6 +1,7 @@
1
1
  import { $hook, $inject, $logger, Alepha } from "@alepha/core";
2
2
  import { type Route, RouterProvider } from "@alepha/router";
3
- import type { ReactNode } from "react";
3
+ import { createElement, type ReactNode } from "react";
4
+ import NotFoundPage from "../components/NotFound.tsx";
4
5
  import {
5
6
  isPageRoute,
6
7
  PageDescriptorProvider,
@@ -98,7 +99,7 @@ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
98
99
  if (state.layers.length === 0) {
99
100
  state.layers.push({
100
101
  name: "not-found",
101
- element: "Not Found",
102
+ element: createElement(NotFoundPage),
102
103
  index: 0,
103
104
  path: "/",
104
105
  });
@@ -5,6 +5,7 @@ import { createElement, type ReactNode, StrictMode } from "react";
5
5
  import ClientOnly from "../components/ClientOnly.tsx";
6
6
  import ErrorViewer from "../components/ErrorViewer.tsx";
7
7
  import NestedView from "../components/NestedView.tsx";
8
+ import NotFoundPage from "../components/NotFound.tsx";
8
9
  import { RouterContext } from "../contexts/RouterContext.ts";
9
10
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
10
11
  import {
@@ -405,18 +406,37 @@ export class PageDescriptorProvider {
405
406
  protected readonly configure = $hook({
406
407
  name: "configure",
407
408
  handler: () => {
409
+ let hasNotFoundHandler = false;
408
410
  const pages = this.alepha.getDescriptorValues($page);
409
411
  for (const { value, key } of pages) {
410
412
  value[OPTIONS].name ??= key;
411
413
  }
414
+
412
415
  for (const { value } of pages) {
413
416
  // skip children, we only want root pages
414
417
  if (value[OPTIONS].parent) {
415
418
  continue;
416
419
  }
417
420
 
421
+ if (value[OPTIONS].path === "/*") {
422
+ hasNotFoundHandler = true;
423
+ }
424
+
418
425
  this.add(this.map(pages, value));
419
426
  }
427
+
428
+ if (!hasNotFoundHandler && pages.length > 0) {
429
+ // add a default 404 page if not already defined
430
+ this.add({
431
+ path: "/*",
432
+ name: "notFound",
433
+ cache: true,
434
+ component: NotFoundPage,
435
+ afterHandler: ({ reply }) => {
436
+ reply.status = 404;
437
+ },
438
+ });
439
+ }
420
440
  },
421
441
  });
422
442
 
@@ -1,7 +1,6 @@
1
- import { $hook, $inject, $logger, Alepha, type Static, t } from "@alepha/core";
1
+ import { $hook, $inject, $logger, Alepha } from "@alepha/core";
2
2
  import { type ApiLinksResponse, HttpClient } from "@alepha/server";
3
3
  import type { Root } from "react-dom/client";
4
- import { createRoot, hydrateRoot } from "react-dom/client";
5
4
  import { BrowserHeadProvider } from "./BrowserHeadProvider.ts";
6
5
  import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
7
6
  import type {
@@ -11,21 +10,12 @@ import type {
11
10
  TransitionOptions,
12
11
  } from "./PageDescriptorProvider.ts";
13
12
 
14
- const envSchema = t.object({
15
- REACT_ROOT_ID: t.string({ default: "root" }),
16
- });
17
-
18
- declare module "@alepha/core" {
19
- interface Env extends Partial<Static<typeof envSchema>> {}
20
- }
21
-
22
13
  export class ReactBrowserProvider {
23
14
  protected readonly log = $logger();
24
15
  protected readonly client = $inject(HttpClient);
25
16
  protected readonly alepha = $inject(Alepha);
26
17
  protected readonly router = $inject(BrowserRouterProvider);
27
18
  protected readonly headProvider = $inject(BrowserHeadProvider);
28
- protected readonly env = $inject(envSchema);
29
19
  protected root!: Root;
30
20
 
31
21
  public transitioning?: {
@@ -75,11 +65,6 @@ export class ReactBrowserProvider {
75
65
  await this.render({ previous });
76
66
  }
77
67
 
78
- /**
79
- *
80
- * @param url
81
- * @param options
82
- */
83
68
  public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
84
69
  const result = await this.render({
85
70
  url,
@@ -127,8 +112,6 @@ export class ReactBrowserProvider {
127
112
 
128
113
  /**
129
114
  * Get embedded layers from the server.
130
- *
131
- * @protected
132
115
  */
133
116
  protected getHydrationState(): ReactHydrationState | undefined {
134
117
  try {
@@ -140,30 +123,8 @@ export class ReactBrowserProvider {
140
123
  }
141
124
  }
142
125
 
143
- /**
144
- *
145
- * @protected
146
- */
147
- protected getRootElement() {
148
- const root = this.document.getElementById(this.env.REACT_ROOT_ID);
149
- if (root) {
150
- return root;
151
- }
152
-
153
- const div = this.document.createElement("div");
154
- div.id = this.env.REACT_ROOT_ID;
155
-
156
- this.document.body.prepend(div);
157
-
158
- return div;
159
- }
160
-
161
126
  // -------------------------------------------------------------------------------------------------------------------
162
127
 
163
- /**
164
- *
165
- * @protected
166
- */
167
128
  public readonly ready = $hook({
168
129
  name: "ready",
169
130
  handler: async () => {
@@ -187,17 +148,6 @@ export class ReactBrowserProvider {
187
148
  hydration,
188
149
  });
189
150
 
190
- const element = this.router.root(this.state, context);
191
-
192
- if (previous.length > 0) {
193
- this.root = hydrateRoot(this.getRootElement(), element);
194
- this.log.info("Hydrated root element");
195
- } else {
196
- this.root ??= createRoot(this.getRootElement());
197
- this.root.render(element);
198
- this.log.info("Created root element");
199
- }
200
-
201
151
  window.addEventListener("popstate", () => {
202
152
  this.render();
203
153
  });
@@ -0,0 +1,72 @@
1
+ import { $hook, $inject, $logger, type Static, t } from "@alepha/core";
2
+ import type { ApiLinksResponse } from "@alepha/server";
3
+ import type { Root } from "react-dom/client";
4
+ import { createRoot, hydrateRoot } from "react-dom/client";
5
+ import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
6
+ import type {
7
+ PreviousLayerData,
8
+ TransitionOptions,
9
+ } from "./PageDescriptorProvider.ts";
10
+ import { ReactBrowserProvider } from "./ReactBrowserProvider.ts";
11
+
12
+ const envSchema = t.object({
13
+ REACT_ROOT_ID: t.string({ default: "root" }),
14
+ });
15
+
16
+ declare module "@alepha/core" {
17
+ interface Env extends Partial<Static<typeof envSchema>> {}
18
+ }
19
+
20
+ export class ReactBrowserRenderer {
21
+ protected readonly browserProvider = $inject(ReactBrowserProvider);
22
+ protected readonly browserRouterProvider = $inject(BrowserRouterProvider);
23
+ protected readonly env = $inject(envSchema);
24
+ protected readonly log = $logger();
25
+
26
+ protected root!: Root;
27
+
28
+ protected getRootElement() {
29
+ const root = this.browserProvider.document.getElementById(
30
+ this.env.REACT_ROOT_ID,
31
+ );
32
+ if (root) {
33
+ return root;
34
+ }
35
+
36
+ const div = this.browserProvider.document.createElement("div");
37
+ div.id = this.env.REACT_ROOT_ID;
38
+
39
+ this.browserProvider.document.body.prepend(div);
40
+
41
+ return div;
42
+ }
43
+
44
+ public readonly ready = $hook({
45
+ name: "react:browser:render",
46
+ handler: async ({ state, context, hydration }) => {
47
+ const element = this.browserRouterProvider.root(state, context);
48
+
49
+ if (hydration?.layers) {
50
+ this.root = hydrateRoot(this.getRootElement(), element);
51
+ this.log.info("Hydrated root element");
52
+ } else {
53
+ this.root ??= createRoot(this.getRootElement());
54
+ this.root.render(element);
55
+ this.log.info("Created root element");
56
+ }
57
+ },
58
+ });
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------------------------------------------------
62
+
63
+ export interface RouterGoOptions {
64
+ replace?: boolean;
65
+ match?: TransitionOptions;
66
+ params?: Record<string, string>;
67
+ }
68
+
69
+ export interface ReactHydrationState {
70
+ layers?: Array<PreviousLayerData>;
71
+ links?: ApiLinksResponse;
72
+ }
@@ -120,7 +120,6 @@ export class ReactServerProvider {
120
120
  }
121
121
 
122
122
  reply.headers["content-type"] = "text/html";
123
- reply.status = 200;
124
123
 
125
124
  // serve index.html for all unmatched routes
126
125
  return this.template;
@@ -312,7 +311,6 @@ export class ReactServerProvider {
312
311
  return reply.redirect(state.redirect);
313
312
  }
314
313
 
315
- reply.status = 200;
316
314
  reply.headers["content-type"] = "text/html";
317
315
 
318
316
  // by default, disable caching for SSR responses
@@ -327,7 +325,11 @@ export class ReactServerProvider {
327
325
  delete context.links;
328
326
  }
329
327
 
330
- return this.renderToHtml(template, state, context);
328
+ const html = this.renderToHtml(template, state, context);
329
+
330
+ page.afterHandler?.(serverRequest);
331
+
332
+ return html;
331
333
  };
332
334
  }
333
335