@alepha/react 0.6.10 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/index.browser.cjs +21 -20
  3. package/dist/index.browser.js +2 -3
  4. package/dist/index.cjs +168 -82
  5. package/dist/index.d.ts +415 -232
  6. package/dist/index.js +146 -62
  7. package/dist/{useActive-4QlZKGbw.cjs → useRouterState-AdK-XeM2.cjs} +358 -170
  8. package/dist/{useActive-ClUsghB5.js → useRouterState-qoMq7Y9J.js} +358 -172
  9. package/package.json +11 -10
  10. package/src/components/ClientOnly.tsx +35 -0
  11. package/src/components/ErrorBoundary.tsx +72 -0
  12. package/src/components/ErrorViewer.tsx +161 -0
  13. package/src/components/Link.tsx +10 -4
  14. package/src/components/NestedView.tsx +28 -4
  15. package/src/descriptors/$page.ts +143 -38
  16. package/src/errors/RedirectionError.ts +4 -1
  17. package/src/hooks/RouterHookApi.ts +58 -35
  18. package/src/hooks/useAlepha.ts +12 -0
  19. package/src/hooks/useClient.ts +8 -6
  20. package/src/hooks/useInject.ts +3 -9
  21. package/src/hooks/useQueryParams.ts +4 -7
  22. package/src/hooks/useRouter.ts +6 -0
  23. package/src/index.browser.ts +1 -1
  24. package/src/index.shared.ts +11 -4
  25. package/src/index.ts +7 -4
  26. package/src/providers/BrowserRouterProvider.ts +27 -33
  27. package/src/providers/PageDescriptorProvider.ts +90 -40
  28. package/src/providers/ReactBrowserProvider.ts +21 -27
  29. package/src/providers/ReactServerProvider.ts +215 -77
  30. package/dist/index.browser.cjs.map +0 -1
  31. package/dist/index.browser.js.map +0 -1
  32. package/dist/index.cjs.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/useActive-4QlZKGbw.cjs.map +0 -1
  35. package/dist/useActive-ClUsghB5.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
- "version": "0.6.10",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -13,25 +13,26 @@
13
13
  "src"
14
14
  ],
15
15
  "dependencies": {
16
- "@alepha/core": "0.6.10",
17
- "@alepha/router": "0.6.10",
18
- "@alepha/server": "0.6.10",
19
- "@alepha/server-static": "0.6.10",
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",
20
20
  "react-dom": "^19.1.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/react": "^19.1.4",
24
- "@types/react-dom": "^19.1.5",
25
- "pkgroll": "^2.12.2",
23
+ "@types/react": "^19.1.8",
24
+ "@types/react-dom": "^19.1.6",
25
+ "pkgroll": "^2.13.1",
26
26
  "react": "^19.1.0",
27
- "vitest": "^3.1.3"
27
+ "vitest": "^3.2.4"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@types/react": "^19",
31
31
  "react": "^19"
32
32
  },
33
33
  "scripts": {
34
- "build": "pkgroll --clean-dist --sourcemap"
34
+ "test": "vitest run",
35
+ "build": "pkgroll --clean-dist"
35
36
  },
36
37
  "homepage": "https://github.com/feunard/alepha",
37
38
  "repository": {
@@ -0,0 +1,35 @@
1
+ import {
2
+ type PropsWithChildren,
3
+ type ReactNode,
4
+ useEffect,
5
+ useState,
6
+ } from "react";
7
+
8
+ export interface ClientOnlyProps {
9
+ fallback?: ReactNode;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ /**
14
+ * A small utility component that renders its children only on the client side.
15
+ *
16
+ * Optionally, you can provide a fallback React node that will be rendered.
17
+ *
18
+ * You should use this component when
19
+ * - you have code that relies on browser-specific APIs
20
+ * - you want to avoid server-side rendering for a specific part of your application
21
+ * - you want to prevent pre-rendering of a component
22
+ */
23
+ const ClientOnly = (props: PropsWithChildren<ClientOnlyProps>) => {
24
+ const [mounted, setMounted] = useState(false);
25
+
26
+ useEffect(() => setMounted(true), []);
27
+
28
+ if (props.disabled) {
29
+ return props.children;
30
+ }
31
+
32
+ return mounted ? props.children : props.fallback;
33
+ };
34
+
35
+ export default ClientOnly;
@@ -0,0 +1,72 @@
1
+ import React, {
2
+ type ErrorInfo,
3
+ type PropsWithChildren,
4
+ type ReactNode,
5
+ } from "react";
6
+
7
+ /**
8
+ * Props for the ErrorBoundary component.
9
+ */
10
+ export interface ErrorBoundaryProps {
11
+ /**
12
+ * Fallback React node to render when an error is caught.
13
+ * If not provided, a default error message will be shown.
14
+ */
15
+ fallback: (error: Error) => ReactNode;
16
+
17
+ /**
18
+ * Optional callback that receives the error and error info.
19
+ * Use this to log errors to a monitoring service.
20
+ */
21
+ onError?: (error: Error, info: ErrorInfo) => void;
22
+ }
23
+
24
+ /**
25
+ * State of the ErrorBoundary component.
26
+ */
27
+ interface ErrorBoundaryState {
28
+ error?: Error;
29
+ }
30
+
31
+ /**
32
+ * A reusable error boundary for catching rendering errors
33
+ * in any part of the React component tree.
34
+ */
35
+ export class ErrorBoundary extends React.Component<
36
+ PropsWithChildren<ErrorBoundaryProps>,
37
+ ErrorBoundaryState
38
+ > {
39
+ constructor(props: ErrorBoundaryProps) {
40
+ super(props);
41
+ this.state = {};
42
+ }
43
+
44
+ /**
45
+ * Update state so the next render shows the fallback UI.
46
+ */
47
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
48
+ return {
49
+ error,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Lifecycle method called when an error is caught.
55
+ * You can log the error or perform side effects here.
56
+ */
57
+ componentDidCatch(error: Error, info: ErrorInfo): void {
58
+ if (this.props.onError) {
59
+ this.props.onError(error, info);
60
+ }
61
+ }
62
+
63
+ render(): ReactNode {
64
+ if (this.state.error) {
65
+ return this.props.fallback(this.state.error);
66
+ }
67
+
68
+ return this.props.children;
69
+ }
70
+ }
71
+
72
+ export default ErrorBoundary;
@@ -0,0 +1,161 @@
1
+ import { useState } from "react";
2
+ import { useAlepha } from "../hooks/useAlepha.ts";
3
+
4
+ interface ErrorViewerProps {
5
+ error: Error;
6
+ }
7
+
8
+ // TODO: design this better
9
+
10
+ const ErrorViewer = ({ error }: ErrorViewerProps) => {
11
+ const [expanded, setExpanded] = useState(false);
12
+ const isProduction = useAlepha().isProduction();
13
+ // const status = isHttpError(error) ? error.status : 500;
14
+
15
+ if (isProduction) {
16
+ return <ErrorViewerProduction />;
17
+ }
18
+
19
+ const stackLines = error.stack?.split("\n") ?? [];
20
+ const previewLines = stackLines.slice(0, 5);
21
+ const hiddenLineCount = stackLines.length - previewLines.length;
22
+
23
+ const copyToClipboard = (text: string) => {
24
+ navigator.clipboard.writeText(text).catch((err) => {
25
+ console.error("Clipboard error:", err);
26
+ });
27
+ };
28
+
29
+ const styles = {
30
+ container: {
31
+ padding: "24px",
32
+ backgroundColor: "#FEF2F2",
33
+ color: "#7F1D1D",
34
+ border: "1px solid #FECACA",
35
+ borderRadius: "16px",
36
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
37
+ fontFamily: "monospace",
38
+ maxWidth: "768px",
39
+ margin: "40px auto",
40
+ },
41
+ heading: {
42
+ fontSize: "20px",
43
+ fontWeight: "bold",
44
+ marginBottom: "4px",
45
+ },
46
+ name: {
47
+ fontSize: "16px",
48
+ fontWeight: 600,
49
+ },
50
+ message: {
51
+ fontSize: "14px",
52
+ marginBottom: "16px",
53
+ },
54
+ sectionHeader: {
55
+ display: "flex",
56
+ justifyContent: "space-between",
57
+ alignItems: "center",
58
+ fontSize: "12px",
59
+ marginBottom: "4px",
60
+ color: "#991B1B",
61
+ },
62
+ copyButton: {
63
+ fontSize: "12px",
64
+ color: "#DC2626",
65
+ background: "none",
66
+ border: "none",
67
+ cursor: "pointer",
68
+ textDecoration: "underline",
69
+ },
70
+ stackContainer: {
71
+ backgroundColor: "#FEE2E2",
72
+ padding: "12px",
73
+ borderRadius: "8px",
74
+ fontSize: "13px",
75
+ lineHeight: "1.4",
76
+ overflowX: "auto" as const,
77
+ whiteSpace: "pre-wrap" as const,
78
+ },
79
+ expandLine: {
80
+ color: "#F87171",
81
+ cursor: "pointer",
82
+ marginTop: "8px",
83
+ },
84
+ };
85
+
86
+ return (
87
+ <div style={styles.container}>
88
+ <div>
89
+ <div style={styles.heading}>🔥 Error</div>
90
+ <div style={styles.name}>{error.name}</div>
91
+ <div style={styles.message}>{error.message}</div>
92
+ </div>
93
+
94
+ {stackLines.length > 0 && (
95
+ <div>
96
+ <div style={styles.sectionHeader}>
97
+ <span>Stack trace</span>
98
+ <button
99
+ onClick={() => copyToClipboard(error.stack!)}
100
+ style={styles.copyButton}
101
+ >
102
+ Copy all
103
+ </button>
104
+ </div>
105
+ <pre style={styles.stackContainer}>
106
+ {(expanded ? stackLines : previewLines).map((line, i) => (
107
+ <div key={i}>{line}</div>
108
+ ))}
109
+ {!expanded && hiddenLineCount > 0 && (
110
+ <div style={styles.expandLine} onClick={() => setExpanded(true)}>
111
+ + {hiddenLineCount} more lines...
112
+ </div>
113
+ )}
114
+ </pre>
115
+ </div>
116
+ )}
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export default ErrorViewer;
122
+
123
+ const ErrorViewerProduction = () => {
124
+ const styles = {
125
+ container: {
126
+ padding: "24px",
127
+ backgroundColor: "#FEF2F2",
128
+ color: "#7F1D1D",
129
+ border: "1px solid #FECACA",
130
+ borderRadius: "16px",
131
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
132
+ fontFamily: "monospace",
133
+ maxWidth: "768px",
134
+ margin: "40px auto",
135
+ textAlign: "center" as const,
136
+ },
137
+ heading: {
138
+ fontSize: "20px",
139
+ fontWeight: "bold",
140
+ marginBottom: "8px",
141
+ },
142
+ name: {
143
+ fontSize: "16px",
144
+ fontWeight: 600,
145
+ marginBottom: "4px",
146
+ },
147
+ message: {
148
+ fontSize: "14px",
149
+ opacity: 0.85,
150
+ },
151
+ };
152
+
153
+ return (
154
+ <div style={styles.container}>
155
+ <div style={styles.heading}>🚨 An error occurred</div>
156
+ <div style={styles.message}>
157
+ Something went wrong. Please try again later.
158
+ </div>
159
+ </div>
160
+ );
161
+ };
@@ -1,9 +1,9 @@
1
- import React from "react";
1
+ import { OPTIONS } from "@alepha/core";
2
2
  import type { AnchorHTMLAttributes } from "react";
3
+ import React from "react";
3
4
  import { RouterContext } from "../contexts/RouterContext.ts";
4
5
  import type { PageDescriptor } from "../descriptors/$page.ts";
5
6
  import { useRouter } from "../hooks/useRouter.ts";
6
- import { OPTIONS } from "@alepha/core";
7
7
 
8
8
  export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
9
9
  to: string | PageDescriptor;
@@ -13,6 +13,8 @@ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
13
13
  const Link = (props: LinkProps) => {
14
14
  React.useContext(RouterContext);
15
15
 
16
+ const router = useRouter();
17
+
16
18
  const to = typeof props.to === "string" ? props.to : props.to[OPTIONS].path;
17
19
  if (!to) {
18
20
  return null;
@@ -26,9 +28,13 @@ const Link = (props: LinkProps) => {
26
28
  const name =
27
29
  typeof props.to === "string" ? undefined : props.to[OPTIONS].name;
28
30
 
29
- const router = useRouter();
31
+ const anchorProps = {
32
+ ...props,
33
+ to: undefined,
34
+ };
35
+
30
36
  return (
31
- <a {...router.createAnchorProps(to)} {...props}>
37
+ <a {...router.anchor(to)} {...anchorProps}>
32
38
  {props.children ?? name}
33
39
  </a>
34
40
  );
@@ -3,16 +3,32 @@ import { useContext, useState } from "react";
3
3
  import { RouterContext } from "../contexts/RouterContext.ts";
4
4
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
5
5
  import { useRouterEvents } from "../hooks/useRouterEvents.ts";
6
+ import ErrorBoundary from "./ErrorBoundary.tsx";
6
7
 
7
8
  export interface NestedViewProps {
8
9
  children?: ReactNode;
9
10
  }
10
11
 
11
12
  /**
12
- * Nested view component
13
+ * A component that renders the current view of the nested router layer.
13
14
  *
14
- * @param props
15
- * @constructor
15
+ * To be simple, it renders the `element` of the current child page of a parent page.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { NestedView } from "@alepha/react";
20
+ *
21
+ * class App {
22
+ * parent = $page({
23
+ * component: () => <NestedView />,
24
+ * });
25
+ *
26
+ * child = $page({
27
+ * parent: this.root,
28
+ * component: () => <div>Child Page</div>,
29
+ * });
30
+ * }
31
+ * ```
16
32
  */
17
33
  const NestedView = (props: NestedViewProps) => {
18
34
  const app = useContext(RouterContext);
@@ -32,7 +48,15 @@ const NestedView = (props: NestedViewProps) => {
32
48
  [app],
33
49
  );
34
50
 
35
- return view ?? props.children ?? null;
51
+ if (!app) {
52
+ throw new Error("NestedView must be used within a RouterContext.");
53
+ }
54
+
55
+ const element = view ?? props.children ?? null;
56
+
57
+ return (
58
+ <ErrorBoundary fallback={app.context.onError!}>{element}</ErrorBoundary>
59
+ );
36
60
  };
37
61
 
38
62
  export default NestedView;
@@ -1,7 +1,16 @@
1
- import { type Async, OPTIONS, type Static, type TSchema } from "@alepha/core";
2
- import { KIND, NotImplementedError, __descriptor } from "@alepha/core";
3
- import type { FC } from "react";
4
- import type { RouterHookApi } from "../hooks/RouterHookApi.ts";
1
+ import {
2
+ __descriptor,
3
+ type Async,
4
+ KIND,
5
+ NotImplementedError,
6
+ OPTIONS,
7
+ type Static,
8
+ type TSchema,
9
+ } from "@alepha/core";
10
+ import type { ServerRoute } from "@alepha/server";
11
+ import type { FC, ReactNode } from "react";
12
+ import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
13
+ import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
5
14
 
6
15
  const KEY = "PAGE";
7
16
 
@@ -12,34 +21,100 @@ export interface PageConfigSchema {
12
21
 
13
22
  export type TPropsDefault = any;
14
23
 
15
- export type TPropsParentDefault = object;
24
+ export type TPropsParentDefault = {};
16
25
 
17
26
  export interface PageDescriptorOptions<
18
27
  TConfig extends PageConfigSchema = PageConfigSchema,
19
28
  TProps extends object = TPropsDefault,
20
29
  TPropsParent extends object = TPropsParentDefault,
21
- > {
30
+ > extends Pick<ServerRoute, "cache"> {
31
+ /**
32
+ * Name your page.
33
+ *
34
+ * @default Descriptor key
35
+ */
22
36
  name?: string;
23
37
 
38
+ /**
39
+ * Optional description of the page.
40
+ */
41
+ description?: string;
42
+
43
+ /**
44
+ * Add a pathname to the page.
45
+ *
46
+ * Pathname can contain parameters, like `/post/:slug`.
47
+ *
48
+ * @default ""
49
+ */
24
50
  path?: string;
25
51
 
52
+ /**
53
+ * Add an input schema to define:
54
+ * - `params`: parameters from the pathname.
55
+ * - `query`: query parameters from the URL.
56
+ */
26
57
  schema?: TConfig;
27
58
 
28
- resolve?: (config: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
29
-
59
+ /**
60
+ * Load data before rendering the page.
61
+ *
62
+ * This function receives
63
+ * - the request context and
64
+ * - the parent props (if page has a parent)
65
+ *
66
+ * In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
67
+ *
68
+ * Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
69
+ * It's common to throw a `NotFoundError` to display a 404 page.
70
+ *
71
+ * RedirectError can be thrown to redirect the user to another page.
72
+ */
73
+ resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
74
+
75
+ /**
76
+ * The component to render when the page is loaded.
77
+ *
78
+ * If `lazy` is defined, this will be ignored.
79
+ * Prefer using `lazy` to improve the initial loading time.
80
+ */
30
81
  component?: FC<TProps & TPropsParent>;
31
82
 
83
+ /**
84
+ * Lazy load the component when the page is loaded.
85
+ *
86
+ * It's recommended to use this for components to improve the initial loading time
87
+ * and enable code-splitting.
88
+ */
32
89
  lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
33
90
 
91
+ /**
92
+ * Set some children pages and make the page a parent page.
93
+ *
94
+ * /!\ Parent page can't be rendered directly. /!\
95
+ *
96
+ * If you still want to render at this pathname, add a child page with an empty path.
97
+ */
34
98
  children?: Array<{ [OPTIONS]: PageDescriptorOptions }>;
35
99
 
36
- parent?: { [OPTIONS]: PageDescriptorOptions<any, TPropsParent> };
100
+ parent?: { [OPTIONS]: PageDescriptorOptions<PageConfigSchema, TPropsParent> };
37
101
 
38
102
  can?: () => boolean;
39
103
 
40
104
  head?: Head | ((props: TProps, previous?: Head) => Head);
41
105
 
42
- errorHandler?: FC<{ error: Error; url: string }>;
106
+ errorHandler?: (error: Error) => ReactNode;
107
+
108
+ prerender?:
109
+ | boolean
110
+ | {
111
+ entries?: Array<Partial<PageRequestConfig<TConfig>>>;
112
+ };
113
+
114
+ /**
115
+ * If true, the page will be rendered on the client-side.
116
+ */
117
+ client?: boolean | ClientOnlyProps;
43
118
  }
44
119
 
45
120
  export interface PageDescriptor<
@@ -50,18 +125,18 @@ export interface PageDescriptor<
50
125
  [KIND]: typeof KEY;
51
126
  [OPTIONS]: PageDescriptorOptions<TConfig, TProps, TPropsParent>;
52
127
 
53
- render: (options?: {
54
- params?: Record<string, string>;
55
- query?: Record<string, string>;
56
- }) => Promise<string>;
57
- go: () => void;
58
- createAnchorProps: (routerHook: RouterHookApi) => {
59
- href: string;
60
- onClick: () => void;
61
- };
62
- can: () => boolean;
128
+ /**
129
+ * For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
130
+ * Only valid for server-side rendering, it will throw an error if called on the client-side.
131
+ */
132
+ render: (
133
+ options?: PageDescriptorRenderOptions,
134
+ ) => Promise<PageDescriptorRenderResult>;
63
135
  }
64
136
 
137
+ /**
138
+ * Main descriptor for defining a React route in the application.
139
+ */
65
140
  export const $page = <
66
141
  TConfig extends PageConfigSchema = PageConfigSchema,
67
142
  TProps extends object = TPropsDefault,
@@ -92,18 +167,6 @@ export const $page = <
92
167
  render: () => {
93
168
  throw new NotImplementedError(KEY);
94
169
  },
95
- go: () => {
96
- throw new NotImplementedError(KEY);
97
- },
98
- createAnchorProps: () => {
99
- throw new NotImplementedError(KEY);
100
- },
101
- can: () => {
102
- if (options.can) {
103
- return options.can();
104
- }
105
- return true;
106
- },
107
170
  };
108
171
  };
109
172
 
@@ -111,12 +174,59 @@ $page[KIND] = KEY;
111
174
 
112
175
  // ---------------------------------------------------------------------------------------------------------------------
113
176
 
177
+ export interface PageDescriptorRenderOptions {
178
+ params?: Record<string, string>;
179
+ query?: Record<string, string>;
180
+ withLayout?: boolean;
181
+ }
182
+
183
+ export interface PageDescriptorRenderResult {
184
+ html: string;
185
+ context: PageReactContext;
186
+ }
187
+
114
188
  export interface Head {
115
189
  title?: string;
190
+ description?: string;
116
191
  titleSeparator?: string;
117
192
  htmlAttributes?: Record<string, string>;
118
193
  bodyAttributes?: Record<string, string>;
119
194
  meta?: Array<{ name: string; content: string }>;
195
+
196
+ // TODO
197
+ keywords?: string[];
198
+ author?: string;
199
+ robots?: string;
200
+ themeColor?: string;
201
+ viewport?:
202
+ | string
203
+ | {
204
+ width?: string;
205
+ height?: string;
206
+ initialScale?: string;
207
+ maximumScale?: string;
208
+ userScalable?: "no" | "yes" | "0" | "1";
209
+ interactiveWidget?:
210
+ | "resizes-visual"
211
+ | "resizes-content"
212
+ | "overlays-content";
213
+ };
214
+
215
+ og?: {
216
+ title?: string;
217
+ description?: string;
218
+ image?: string;
219
+ url?: string;
220
+ type?: string;
221
+ };
222
+
223
+ twitter?: {
224
+ card?: string;
225
+ title?: string;
226
+ description?: string;
227
+ image?: string;
228
+ site?: string;
229
+ };
120
230
  }
121
231
 
122
232
  export interface PageRequestConfig<
@@ -134,9 +244,4 @@ export interface PageRequestConfig<
134
244
  export type PageResolve<
135
245
  TConfig extends PageConfigSchema = PageConfigSchema,
136
246
  TPropsParent extends object = TPropsParentDefault,
137
- > = PageRequestConfig<TConfig> & TPropsParent & PageResolveContext;
138
-
139
- export interface PageResolveContext {
140
- url: URL;
141
- head: Head;
142
- }
247
+ > = PageRequestConfig<TConfig> & TPropsParent & PageReactContext;
@@ -1,7 +1,10 @@
1
1
  import type { HrefLike } from "../hooks/RouterHookApi.ts";
2
2
 
3
3
  export class RedirectionError extends Error {
4
- constructor(public readonly page: HrefLike) {
4
+ public readonly page: HrefLike;
5
+
6
+ constructor(page: HrefLike) {
5
7
  super("Redirection");
8
+ this.page = page;
6
9
  }
7
10
  }