@alepha/react 0.7.1 → 0.7.2
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.browser.cjs +40 -3
- package/dist/index.browser.js +42 -5
- package/dist/index.cjs +4 -5
- package/dist/index.d.ts +5 -49
- package/dist/index.js +5 -6
- package/dist/{useRouterState-AdK-XeM2.cjs → useRouterState-C2uo0jXu.cjs} +51 -61
- package/dist/{useRouterState-qoMq7Y9J.js → useRouterState-D5__ZcUV.js} +51 -61
- package/package.json +5 -5
- package/src/components/NotFound.tsx +30 -0
- package/src/descriptors/$page.ts +3 -1
- package/src/hooks/RouterHookApi.ts +0 -19
- package/src/index.browser.ts +3 -1
- package/src/providers/BrowserRouterProvider.ts +3 -2
- package/src/providers/PageDescriptorProvider.ts +20 -0
- package/src/providers/ReactBrowserProvider.ts +1 -51
- package/src/providers/ReactBrowserRenderer.ts +72 -0
- package/src/providers/ReactServerProvider.ts +5 -3
package/dist/index.browser.cjs
CHANGED
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var core = require('@alepha/core');
|
|
4
|
-
var useRouterState = require('./useRouterState-
|
|
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);
|
package/dist/index.browser.js
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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-
|
|
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,13 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@alepha/core');
|
|
4
4
|
var server = require('@alepha/server');
|
|
5
|
-
var useRouterState = require('./useRouterState-
|
|
5
|
+
var useRouterState = require('./useRouterState-C2uo0jXu.cjs');
|
|
6
6
|
var node_fs = require('node:fs');
|
|
7
7
|
var node_path = require('node:path');
|
|
8
8
|
var serverStatic = require('@alepha/server-static');
|
|
9
9
|
var server$1 = require('react-dom/server');
|
|
10
10
|
require('react/jsx-runtime');
|
|
11
11
|
require('react');
|
|
12
|
-
require('react-dom/client');
|
|
13
12
|
require('@alepha/router');
|
|
14
13
|
|
|
15
14
|
class ServerHeadProvider {
|
|
@@ -138,7 +137,6 @@ class ReactServerProvider {
|
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
139
|
reply.headers["content-type"] = "text/html";
|
|
141
|
-
reply.status = 200;
|
|
142
140
|
return this.template;
|
|
143
141
|
}
|
|
144
142
|
});
|
|
@@ -278,7 +276,6 @@ class ReactServerProvider {
|
|
|
278
276
|
if (state.redirect) {
|
|
279
277
|
return reply.redirect(state.redirect);
|
|
280
278
|
}
|
|
281
|
-
reply.status = 200;
|
|
282
279
|
reply.headers["content-type"] = "text/html";
|
|
283
280
|
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
284
281
|
reply.headers.pragma = "no-cache";
|
|
@@ -286,7 +283,9 @@ class ReactServerProvider {
|
|
|
286
283
|
if (page.cache && serverRequest.user) {
|
|
287
284
|
delete context.links;
|
|
288
285
|
}
|
|
289
|
-
|
|
286
|
+
const html = this.renderToHtml(template, state, context);
|
|
287
|
+
page.afterHandler?.(serverRequest);
|
|
288
|
+
return html;
|
|
290
289
|
};
|
|
291
290
|
}
|
|
292
291
|
renderToHtml(template, state, context) {
|
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
|
|
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$
|
|
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$
|
|
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,13 @@
|
|
|
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-
|
|
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-
|
|
3
|
+
import { P as PageDescriptorProvider, $ as $page } from './useRouterState-D5__ZcUV.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-D5__ZcUV.js';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ServerStaticProvider } from '@alepha/server-static';
|
|
8
8
|
import { renderToString } from 'react-dom/server';
|
|
9
9
|
import 'react/jsx-runtime';
|
|
10
10
|
import 'react';
|
|
11
|
-
import 'react-dom/client';
|
|
12
11
|
import '@alepha/router';
|
|
13
12
|
|
|
14
13
|
class ServerHeadProvider {
|
|
@@ -137,7 +136,6 @@ class ReactServerProvider {
|
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
138
|
reply.headers["content-type"] = "text/html";
|
|
140
|
-
reply.status = 200;
|
|
141
139
|
return this.template;
|
|
142
140
|
}
|
|
143
141
|
});
|
|
@@ -277,7 +275,6 @@ class ReactServerProvider {
|
|
|
277
275
|
if (state.redirect) {
|
|
278
276
|
return reply.redirect(state.redirect);
|
|
279
277
|
}
|
|
280
|
-
reply.status = 200;
|
|
281
278
|
reply.headers["content-type"] = "text/html";
|
|
282
279
|
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
283
280
|
reply.headers.pragma = "no-cache";
|
|
@@ -285,7 +282,9 @@ class ReactServerProvider {
|
|
|
285
282
|
if (page.cache && serverRequest.user) {
|
|
286
283
|
delete context.links;
|
|
287
284
|
}
|
|
288
|
-
|
|
285
|
+
const html = this.renderToHtml(template, state, context);
|
|
286
|
+
page.afterHandler?.(serverRequest);
|
|
287
|
+
return html;
|
|
289
288
|
};
|
|
290
289
|
}
|
|
291
290
|
renderToHtml(template, state, context) {
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@alepha/core": "0.7.
|
|
17
|
-
"@alepha/router": "0.7.
|
|
18
|
-
"@alepha/server": "0.7.
|
|
19
|
-
"@alepha/server-static": "0.7.
|
|
16
|
+
"@alepha/core": "0.7.2",
|
|
17
|
+
"@alepha/router": "0.7.2",
|
|
18
|
+
"@alepha/server": "0.7.2",
|
|
19
|
+
"@alepha/server-static": "0.7.2",
|
|
20
20
|
"react-dom": "^19.1.0"
|
|
21
21
|
},
|
|
22
22
|
"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
|
+
}
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -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
|
}
|
package/src/index.browser.ts
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
-
|
|
328
|
+
const html = this.renderToHtml(template, state, context);
|
|
329
|
+
|
|
330
|
+
page.afterHandler?.(serverRequest);
|
|
331
|
+
|
|
332
|
+
return html;
|
|
331
333
|
};
|
|
332
334
|
}
|
|
333
335
|
|