@harpy-js/core 0.5.4 → 0.5.6
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/core/app-setup.d.ts +2 -0
- package/dist/core/app-setup.js +45 -7
- package/dist/core/error-pages/default-401.d.ts +6 -0
- package/dist/core/error-pages/default-401.js +21 -0
- package/dist/core/error-pages/default-403.d.ts +6 -0
- package/dist/core/error-pages/default-403.js +21 -0
- package/dist/core/error-pages/default-404.d.ts +7 -0
- package/dist/core/error-pages/default-404.js +26 -0
- package/dist/core/error-pages/default-500.d.ts +7 -0
- package/dist/core/error-pages/default-500.js +28 -0
- package/dist/core/error-pages/error-layout.d.ts +6 -0
- package/dist/core/error-pages/error-layout.js +17 -0
- package/dist/core/jsx-exception.filter.d.ts +14 -0
- package/dist/core/jsx-exception.filter.js +115 -0
- package/dist/core/lazy-route-loader.service.d.ts +28 -0
- package/dist/core/lazy-route-loader.service.js +79 -0
- package/dist/core/lazy-routes.module.d.ts +2 -0
- package/dist/core/lazy-routes.module.js +21 -0
- package/dist/decorators/lazy-route.decorator.d.ts +12 -0
- package/dist/decorators/lazy-route.decorator.js +22 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +13 -1
- package/package.json +1 -1
- package/src/core/app-setup.ts +57 -13
- package/src/core/error-pages/default-401.tsx +43 -0
- package/src/core/error-pages/default-403.tsx +43 -0
- package/src/core/error-pages/default-404.tsx +54 -0
- package/src/core/error-pages/default-500.tsx +59 -0
- package/src/core/error-pages/error-layout.tsx +30 -0
- package/src/core/jsx-exception.filter.ts +130 -0
- package/src/index.ts +9 -0
package/dist/core/app-setup.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
2
|
+
import { ErrorPagesConfig } from "./jsx-exception.filter";
|
|
2
3
|
export interface HarpyAppOptions {
|
|
3
4
|
layout?: any;
|
|
4
5
|
distDir?: string;
|
|
5
6
|
publicDir?: string;
|
|
7
|
+
errorPages?: ErrorPagesConfig;
|
|
6
8
|
}
|
|
7
9
|
export declare function configureHarpyApp(app: NestFastifyApplication, opts?: HarpyAppOptions): Promise<void>;
|
|
8
10
|
export declare function setupHarpyApp(app: NestFastifyApplication, opts?: HarpyAppOptions): Promise<void>;
|
package/dist/core/app-setup.js
CHANGED
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.configureHarpyApp = configureHarpyApp;
|
|
37
40
|
exports.setupHarpyApp = setupHarpyApp;
|
|
@@ -51,12 +54,43 @@ catch (e) {
|
|
|
51
54
|
fastifyCookie = undefined;
|
|
52
55
|
}
|
|
53
56
|
const jsx_engine_1 = require("./jsx.engine");
|
|
57
|
+
const jsx_exception_filter_1 = require("./jsx-exception.filter");
|
|
58
|
+
const react_1 = __importDefault(require("react"));
|
|
59
|
+
const server_1 = require("react-dom/server");
|
|
60
|
+
const default_404_1 = __importDefault(require("./error-pages/default-404"));
|
|
61
|
+
const error_layout_1 = __importDefault(require("./error-pages/error-layout"));
|
|
54
62
|
async function configureHarpyApp(app, opts = {}) {
|
|
55
|
-
const { layout, distDir = "dist", publicDir } = opts;
|
|
63
|
+
const { layout, distDir = "dist", publicDir, errorPages } = opts;
|
|
56
64
|
if (layout) {
|
|
57
65
|
(0, jsx_engine_1.withJsxEngine)(app, layout);
|
|
58
66
|
}
|
|
59
67
|
const fastify = app.getHttpAdapter().getInstance();
|
|
68
|
+
const NotFoundComponent = errorPages?.["404"] || default_404_1.default;
|
|
69
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
70
|
+
if (error?.statusCode === 404 || reply.statusCode === 404) {
|
|
71
|
+
try {
|
|
72
|
+
const props = {
|
|
73
|
+
message: "Page Not Found",
|
|
74
|
+
path: request.url,
|
|
75
|
+
};
|
|
76
|
+
const errorPageContent = react_1.default.createElement(NotFoundComponent, props);
|
|
77
|
+
const wrappedInLayout = react_1.default.createElement(error_layout_1.default, {
|
|
78
|
+
title: "404 - Page Not Found",
|
|
79
|
+
children: errorPageContent,
|
|
80
|
+
});
|
|
81
|
+
const html = (0, server_1.renderToString)(wrappedInLayout);
|
|
82
|
+
void reply
|
|
83
|
+
.status(404)
|
|
84
|
+
.header("Content-Type", "text/html; charset=utf-8")
|
|
85
|
+
.send(`<!DOCTYPE html>${html}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
catch (renderError) {
|
|
89
|
+
console.error("Error rendering 404 page:", renderError);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
});
|
|
60
94
|
if (fastifyCookie) {
|
|
61
95
|
await fastify.register(fastifyCookie);
|
|
62
96
|
}
|
|
@@ -64,14 +98,16 @@ async function configureHarpyApp(app, opts = {}) {
|
|
|
64
98
|
console.warn("[harpy-core] optional dependency `@fastify/cookie` is not installed; skipping cookie registration.");
|
|
65
99
|
}
|
|
66
100
|
if (fastifyStatic) {
|
|
67
|
-
await fastify.register(fastifyStatic, {
|
|
68
|
-
root: path.join(process.cwd(), distDir),
|
|
69
|
-
prefix: "/",
|
|
70
|
-
decorateReply: false,
|
|
71
|
-
});
|
|
72
101
|
if (publicDir) {
|
|
73
102
|
await fastify.register(fastifyStatic, {
|
|
74
|
-
root: path.join(process.cwd(), publicDir),
|
|
103
|
+
root: [path.join(process.cwd(), publicDir), path.join(process.cwd(), distDir)],
|
|
104
|
+
prefix: "/",
|
|
105
|
+
decorateReply: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await fastify.register(fastifyStatic, {
|
|
110
|
+
root: path.join(process.cwd(), distDir),
|
|
75
111
|
prefix: "/",
|
|
76
112
|
decorateReply: false,
|
|
77
113
|
});
|
|
@@ -80,6 +116,8 @@ async function configureHarpyApp(app, opts = {}) {
|
|
|
80
116
|
else {
|
|
81
117
|
console.warn("[harpy-core] optional dependency `@fastify/static` is not installed; static `dist` handler not registered.");
|
|
82
118
|
}
|
|
119
|
+
const exceptionFilter = new jsx_exception_filter_1.JsxExceptionFilter(errorPages);
|
|
120
|
+
app.useGlobalFilters(exceptionFilter);
|
|
83
121
|
}
|
|
84
122
|
async function setupHarpyApp(app, opts = {}) {
|
|
85
123
|
return configureHarpyApp(app, opts);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
export interface UnauthorizedPageProps extends JsxLayoutProps {
|
|
4
|
+
message?: string;
|
|
5
|
+
}
|
|
6
|
+
export default function Default401Page({ message, }: UnauthorizedPageProps): React.JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = Default401Page;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function Default401Page({ message = 'Unauthorized', }) {
|
|
9
|
+
return (react_1.default.createElement("div", { className: "min-h-screen bg-gradient-to-br from-orange-500 to-yellow-400 flex items-center justify-center p-6" },
|
|
10
|
+
react_1.default.createElement("div", { className: "max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12" },
|
|
11
|
+
react_1.default.createElement("div", { className: "mb-8" },
|
|
12
|
+
react_1.default.createElement("div", { className: "inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-orange-500 to-yellow-400 rounded-full shadow-lg mb-6" },
|
|
13
|
+
react_1.default.createElement("span", { className: "text-5xl" }, "\uD83D\uDD12"))),
|
|
14
|
+
react_1.default.createElement("h1", { className: "text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-orange-500 to-yellow-400 mb-4" }, "401"),
|
|
15
|
+
react_1.default.createElement("h2", { className: "text-3xl font-bold text-gray-900 mb-4" }, message),
|
|
16
|
+
react_1.default.createElement("p", { className: "text-lg text-gray-600 mb-8" }, "You need to be authenticated to access this resource."),
|
|
17
|
+
react_1.default.createElement("a", { href: "/login", className: "inline-block px-8 py-3 bg-gradient-to-r from-orange-500 to-yellow-400 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200" }, "Go to Login"),
|
|
18
|
+
react_1.default.createElement("p", { className: "mt-12 text-sm text-gray-500" },
|
|
19
|
+
"Powered by ",
|
|
20
|
+
react_1.default.createElement("span", { className: "text-orange-600 font-semibold" }, "Harpy.js")))));
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
export interface ForbiddenPageProps extends JsxLayoutProps {
|
|
4
|
+
message?: string;
|
|
5
|
+
}
|
|
6
|
+
export default function Default403Page({ message, }: ForbiddenPageProps): React.JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = Default403Page;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function Default403Page({ message = 'Forbidden', }) {
|
|
9
|
+
return (react_1.default.createElement("div", { className: "min-h-screen bg-gradient-to-br from-red-600 to-orange-500 flex items-center justify-center p-6" },
|
|
10
|
+
react_1.default.createElement("div", { className: "max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12" },
|
|
11
|
+
react_1.default.createElement("div", { className: "mb-8" },
|
|
12
|
+
react_1.default.createElement("div", { className: "inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-red-600 to-orange-500 rounded-full shadow-lg mb-6" },
|
|
13
|
+
react_1.default.createElement("span", { className: "text-5xl" }, "\u26D4"))),
|
|
14
|
+
react_1.default.createElement("h1", { className: "text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-red-600 to-orange-500 mb-4" }, "403"),
|
|
15
|
+
react_1.default.createElement("h2", { className: "text-3xl font-bold text-gray-900 mb-4" }, message),
|
|
16
|
+
react_1.default.createElement("p", { className: "text-lg text-gray-600 mb-8" }, "You don't have permission to access this resource."),
|
|
17
|
+
react_1.default.createElement("a", { href: "/", className: "inline-block px-8 py-3 bg-gradient-to-r from-red-600 to-orange-500 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200" }, "Go to Homepage"),
|
|
18
|
+
react_1.default.createElement("p", { className: "mt-12 text-sm text-gray-500" },
|
|
19
|
+
"Powered by ",
|
|
20
|
+
react_1.default.createElement("span", { className: "text-red-600 font-semibold" }, "Harpy.js")))));
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
export interface NotFoundPageProps extends JsxLayoutProps {
|
|
4
|
+
path?: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export default function Default404Page({ path, message, }: NotFoundPageProps): React.JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = Default404Page;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function Default404Page({ path, message = 'Page Not Found', }) {
|
|
9
|
+
return (react_1.default.createElement("div", { className: "min-h-screen bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center p-6" },
|
|
10
|
+
react_1.default.createElement("div", { className: "max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12" },
|
|
11
|
+
react_1.default.createElement("div", { className: "mb-8" },
|
|
12
|
+
react_1.default.createElement("div", { className: "inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-purple-600 to-blue-600 rounded-full shadow-lg mb-6" },
|
|
13
|
+
react_1.default.createElement("span", { className: "text-5xl font-bold text-white" }, "H"))),
|
|
14
|
+
react_1.default.createElement("h1", { className: "text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-600 to-blue-600 mb-4" }, "404"),
|
|
15
|
+
react_1.default.createElement("h2", { className: "text-3xl font-bold text-gray-900 mb-4" }, message),
|
|
16
|
+
react_1.default.createElement("p", { className: "text-lg text-gray-600 mb-8" }, "The page you're looking for doesn't exist or has been moved."),
|
|
17
|
+
path && (react_1.default.createElement("div", { className: "bg-gray-100 border border-gray-300 rounded-lg p-4 mb-8" },
|
|
18
|
+
react_1.default.createElement("p", { className: "font-mono text-sm text-gray-700" },
|
|
19
|
+
react_1.default.createElement("span", { className: "text-gray-500" }, "Requested path:"),
|
|
20
|
+
' ',
|
|
21
|
+
react_1.default.createElement("span", { className: "text-purple-600 font-semibold" }, path)))),
|
|
22
|
+
react_1.default.createElement("a", { href: "/", className: "inline-block px-8 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200" }, "Go to Homepage"),
|
|
23
|
+
react_1.default.createElement("p", { className: "mt-12 text-sm text-gray-500" },
|
|
24
|
+
"Powered by ",
|
|
25
|
+
react_1.default.createElement("span", { className: "text-purple-600 font-semibold" }, "Harpy.js")))));
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
export interface ServerErrorPageProps extends JsxLayoutProps {
|
|
4
|
+
message?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export default function Default500Page({ message, error, }: ServerErrorPageProps): React.JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = Default500Page;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function Default500Page({ message = 'Internal Server Error', error, }) {
|
|
9
|
+
return (react_1.default.createElement("div", { className: "min-h-screen bg-gradient-to-br from-red-500 to-pink-600 flex items-center justify-center p-6" },
|
|
10
|
+
react_1.default.createElement("div", { className: "max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12" },
|
|
11
|
+
react_1.default.createElement("div", { className: "mb-8" },
|
|
12
|
+
react_1.default.createElement("div", { className: "inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-red-500 to-pink-600 rounded-full shadow-lg mb-6" },
|
|
13
|
+
react_1.default.createElement("span", { className: "text-5xl font-bold text-white" }, "H"))),
|
|
14
|
+
react_1.default.createElement("h1", { className: "text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-pink-600 mb-4" }, "500"),
|
|
15
|
+
react_1.default.createElement("h2", { className: "text-3xl font-bold text-gray-900 mb-4" }, message),
|
|
16
|
+
react_1.default.createElement("p", { className: "text-lg text-gray-600 mb-8" }, "Something went wrong on our end. Please try again later."),
|
|
17
|
+
error && (react_1.default.createElement("div", { className: "bg-red-50 border-2 border-red-200 rounded-lg p-6 mb-8 text-left" },
|
|
18
|
+
react_1.default.createElement("div", { className: "flex items-start gap-3" },
|
|
19
|
+
react_1.default.createElement("svg", { className: "w-6 h-6 text-red-600 flex-shrink-0 mt-0.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
20
|
+
react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" })),
|
|
21
|
+
react_1.default.createElement("div", { className: "flex-1" },
|
|
22
|
+
react_1.default.createElement("h3", { className: "text-sm font-semibold text-red-900 mb-2" }, "Error Details"),
|
|
23
|
+
react_1.default.createElement("pre", { className: "text-xs text-red-800 font-mono whitespace-pre-wrap break-words" }, error))))),
|
|
24
|
+
react_1.default.createElement("a", { href: "/", className: "inline-block px-8 py-3 bg-gradient-to-r from-red-500 to-pink-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200" }, "Go to Homepage"),
|
|
25
|
+
react_1.default.createElement("p", { className: "mt-12 text-sm text-gray-500" },
|
|
26
|
+
"Powered by ",
|
|
27
|
+
react_1.default.createElement("span", { className: "text-red-600 font-semibold" }, "Harpy.js")))));
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = ErrorLayout;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function ErrorLayout({ children, title = 'Error', }) {
|
|
9
|
+
return (react_1.default.createElement("html", { lang: "en" },
|
|
10
|
+
react_1.default.createElement("head", null,
|
|
11
|
+
react_1.default.createElement("meta", { charSet: "utf-8" }),
|
|
12
|
+
react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
13
|
+
react_1.default.createElement("title", null, title),
|
|
14
|
+
react_1.default.createElement("link", { rel: "stylesheet", href: "/styles/styles.css" }),
|
|
15
|
+
react_1.default.createElement("link", { rel: "stylesheet", href: "/assets/styles.css" })),
|
|
16
|
+
react_1.default.createElement("body", null, children)));
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ExceptionFilter, ArgumentsHost } from '@nestjs/common';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface ErrorPagesConfig {
|
|
4
|
+
404?: React.ComponentType<any>;
|
|
5
|
+
500?: React.ComponentType<any>;
|
|
6
|
+
401?: React.ComponentType<any>;
|
|
7
|
+
403?: React.ComponentType<any>;
|
|
8
|
+
default?: React.ComponentType<any>;
|
|
9
|
+
}
|
|
10
|
+
export declare class JsxExceptionFilter implements ExceptionFilter {
|
|
11
|
+
private readonly errorPages;
|
|
12
|
+
constructor(errorPages?: ErrorPagesConfig);
|
|
13
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.JsxExceptionFilter = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const react_1 = __importDefault(require("react"));
|
|
18
|
+
const server_1 = require("react-dom/server");
|
|
19
|
+
const default_404_1 = __importDefault(require("./error-pages/default-404"));
|
|
20
|
+
const default_500_1 = __importDefault(require("./error-pages/default-500"));
|
|
21
|
+
const default_401_1 = __importDefault(require("./error-pages/default-401"));
|
|
22
|
+
const default_403_1 = __importDefault(require("./error-pages/default-403"));
|
|
23
|
+
const error_layout_1 = __importDefault(require("./error-pages/error-layout"));
|
|
24
|
+
let JsxExceptionFilter = class JsxExceptionFilter {
|
|
25
|
+
errorPages;
|
|
26
|
+
constructor(errorPages = {}) {
|
|
27
|
+
this.errorPages = errorPages;
|
|
28
|
+
}
|
|
29
|
+
catch(exception, host) {
|
|
30
|
+
const ctx = host.switchToHttp();
|
|
31
|
+
const reply = ctx.getResponse();
|
|
32
|
+
const request = ctx.getRequest();
|
|
33
|
+
let status;
|
|
34
|
+
let message;
|
|
35
|
+
let error;
|
|
36
|
+
if (exception instanceof common_1.HttpException) {
|
|
37
|
+
status = exception.getStatus();
|
|
38
|
+
const response = exception.getResponse();
|
|
39
|
+
if (typeof response === 'string') {
|
|
40
|
+
message = response;
|
|
41
|
+
}
|
|
42
|
+
else if (typeof response === 'object' && response !== null) {
|
|
43
|
+
message =
|
|
44
|
+
response.message || response.error || 'An error occurred';
|
|
45
|
+
error = response.error;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
message = 'An error occurred';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (exception instanceof Error) {
|
|
52
|
+
status = common_1.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
53
|
+
message = 'Internal Server Error';
|
|
54
|
+
error = process.env.NODE_ENV === 'development' ? exception.message : undefined;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
status = common_1.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
58
|
+
message = 'Unknown error occurred';
|
|
59
|
+
}
|
|
60
|
+
let ErrorComponent;
|
|
61
|
+
switch (status) {
|
|
62
|
+
case common_1.HttpStatus.NOT_FOUND:
|
|
63
|
+
ErrorComponent = this.errorPages[404] || default_404_1.default;
|
|
64
|
+
break;
|
|
65
|
+
case common_1.HttpStatus.UNAUTHORIZED:
|
|
66
|
+
ErrorComponent = this.errorPages[401] || default_401_1.default;
|
|
67
|
+
break;
|
|
68
|
+
case common_1.HttpStatus.FORBIDDEN:
|
|
69
|
+
ErrorComponent = this.errorPages[403] || default_403_1.default;
|
|
70
|
+
break;
|
|
71
|
+
case common_1.HttpStatus.INTERNAL_SERVER_ERROR:
|
|
72
|
+
ErrorComponent = this.errorPages[500] || default_500_1.default;
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
ErrorComponent = this.errorPages.default || this.errorPages[500] || default_500_1.default;
|
|
76
|
+
}
|
|
77
|
+
const props = {
|
|
78
|
+
message,
|
|
79
|
+
error,
|
|
80
|
+
path: request.url,
|
|
81
|
+
};
|
|
82
|
+
const titleMap = {
|
|
83
|
+
404: '404 - Page Not Found',
|
|
84
|
+
401: '401 - Unauthorized',
|
|
85
|
+
403: '403 - Forbidden',
|
|
86
|
+
500: '500 - Internal Server Error',
|
|
87
|
+
};
|
|
88
|
+
const title = titleMap[status] || `${status} - Error`;
|
|
89
|
+
try {
|
|
90
|
+
const errorPageContent = react_1.default.createElement(ErrorComponent, props);
|
|
91
|
+
const wrappedInLayout = react_1.default.createElement(error_layout_1.default, {
|
|
92
|
+
title,
|
|
93
|
+
children: errorPageContent,
|
|
94
|
+
});
|
|
95
|
+
const html = (0, server_1.renderToString)(wrappedInLayout);
|
|
96
|
+
void reply
|
|
97
|
+
.status(status)
|
|
98
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
99
|
+
.send(`<!DOCTYPE html>${html}`);
|
|
100
|
+
}
|
|
101
|
+
catch (renderError) {
|
|
102
|
+
console.error('Error rendering JSX error page:', renderError);
|
|
103
|
+
void reply.status(status).send({
|
|
104
|
+
statusCode: status,
|
|
105
|
+
message,
|
|
106
|
+
error,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
exports.JsxExceptionFilter = JsxExceptionFilter;
|
|
112
|
+
exports.JsxExceptionFilter = JsxExceptionFilter = __decorate([
|
|
113
|
+
(0, common_1.Catch)(),
|
|
114
|
+
__metadata("design:paramtypes", [Object])
|
|
115
|
+
], JsxExceptionFilter);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Type } from '@nestjs/common';
|
|
2
|
+
import { LazyModuleLoader } from '@nestjs/core';
|
|
3
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
4
|
+
export interface LazyRouteConfig {
|
|
5
|
+
id: string;
|
|
6
|
+
path: string;
|
|
7
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
8
|
+
moduleLoader: () => Promise<Type<any>>;
|
|
9
|
+
controllerLoader: () => Promise<Type<any>>;
|
|
10
|
+
handlerMethod: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class LazyRouteLoaderService {
|
|
13
|
+
private readonly lazyModuleLoader;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private readonly loadedModules;
|
|
16
|
+
private readonly registeredRoutes;
|
|
17
|
+
constructor(lazyModuleLoader: LazyModuleLoader);
|
|
18
|
+
registerLazyRoute(config: LazyRouteConfig): void;
|
|
19
|
+
getRegisteredRoutes(): LazyRouteConfig[];
|
|
20
|
+
handleLazyRoute(config: LazyRouteConfig, req: FastifyRequest, reply: FastifyReply): Promise<any>;
|
|
21
|
+
isModuleLoaded(moduleId: string): boolean;
|
|
22
|
+
getStatistics(): {
|
|
23
|
+
totalRegistered: number;
|
|
24
|
+
totalLoaded: number;
|
|
25
|
+
loadedModules: string[];
|
|
26
|
+
registeredRoutes: string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var LazyRouteLoaderService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.LazyRouteLoaderService = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
let LazyRouteLoaderService = LazyRouteLoaderService_1 = class LazyRouteLoaderService {
|
|
17
|
+
lazyModuleLoader;
|
|
18
|
+
logger = new common_1.Logger(LazyRouteLoaderService_1.name);
|
|
19
|
+
loadedModules = new Map();
|
|
20
|
+
registeredRoutes = new Map();
|
|
21
|
+
constructor(lazyModuleLoader) {
|
|
22
|
+
this.lazyModuleLoader = lazyModuleLoader;
|
|
23
|
+
}
|
|
24
|
+
registerLazyRoute(config) {
|
|
25
|
+
const routeKey = `${config.method}:${config.path}`;
|
|
26
|
+
this.registeredRoutes.set(routeKey, config);
|
|
27
|
+
this.logger.log(`Registered lazy route: ${routeKey} -> ${config.id}`);
|
|
28
|
+
}
|
|
29
|
+
getRegisteredRoutes() {
|
|
30
|
+
return Array.from(this.registeredRoutes.values());
|
|
31
|
+
}
|
|
32
|
+
async handleLazyRoute(config, req, reply) {
|
|
33
|
+
try {
|
|
34
|
+
let moduleRef = this.loadedModules.get(config.id);
|
|
35
|
+
if (!moduleRef) {
|
|
36
|
+
this.logger.log(`Loading lazy module: ${config.id}...`);
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
const ModuleClass = await config.moduleLoader();
|
|
39
|
+
moduleRef = await this.lazyModuleLoader.load(() => ModuleClass);
|
|
40
|
+
this.loadedModules.set(config.id, moduleRef);
|
|
41
|
+
const loadTime = Date.now() - startTime;
|
|
42
|
+
this.logger.log(`Lazy module ${config.id} loaded in ${loadTime}ms`);
|
|
43
|
+
}
|
|
44
|
+
const ControllerClass = await config.controllerLoader();
|
|
45
|
+
const controller = moduleRef.get(ControllerClass, { strict: false });
|
|
46
|
+
if (!controller) {
|
|
47
|
+
throw new Error(`Controller instance not found in lazy module ${config.id}`);
|
|
48
|
+
}
|
|
49
|
+
const handler = controller[config.handlerMethod];
|
|
50
|
+
if (!handler || typeof handler !== 'function') {
|
|
51
|
+
throw new Error(`Handler method ${config.handlerMethod} not found in controller`);
|
|
52
|
+
}
|
|
53
|
+
const result = await handler.call(controller, req, reply);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
58
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
59
|
+
this.logger.error(`Failed to handle lazy route ${config.id}: ${errorMessage}`, errorStack);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
isModuleLoaded(moduleId) {
|
|
64
|
+
return this.loadedModules.has(moduleId);
|
|
65
|
+
}
|
|
66
|
+
getStatistics() {
|
|
67
|
+
return {
|
|
68
|
+
totalRegistered: this.registeredRoutes.size,
|
|
69
|
+
totalLoaded: this.loadedModules.size,
|
|
70
|
+
loadedModules: Array.from(this.loadedModules.keys()),
|
|
71
|
+
registeredRoutes: Array.from(this.registeredRoutes.keys()),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
exports.LazyRouteLoaderService = LazyRouteLoaderService;
|
|
76
|
+
exports.LazyRouteLoaderService = LazyRouteLoaderService = LazyRouteLoaderService_1 = __decorate([
|
|
77
|
+
(0, common_1.Injectable)(),
|
|
78
|
+
__metadata("design:paramtypes", [core_1.LazyModuleLoader])
|
|
79
|
+
], LazyRouteLoaderService);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.LazyRoutesModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const lazy_route_loader_service_1 = require("./lazy-route-loader.service");
|
|
12
|
+
let LazyRoutesModule = class LazyRoutesModule {
|
|
13
|
+
};
|
|
14
|
+
exports.LazyRoutesModule = LazyRoutesModule;
|
|
15
|
+
exports.LazyRoutesModule = LazyRoutesModule = __decorate([
|
|
16
|
+
(0, common_1.Global)(),
|
|
17
|
+
(0, common_1.Module)({
|
|
18
|
+
providers: [lazy_route_loader_service_1.LazyRouteLoaderService],
|
|
19
|
+
exports: [lazy_route_loader_service_1.LazyRouteLoaderService],
|
|
20
|
+
})
|
|
21
|
+
], LazyRoutesModule);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const LAZY_ROUTE_METADATA = "harpy:lazy-route";
|
|
2
|
+
export interface LazyRouteDecoratorConfig {
|
|
3
|
+
id: string;
|
|
4
|
+
moduleLoader: () => Promise<any>;
|
|
5
|
+
controllerLoader: () => Promise<any>;
|
|
6
|
+
handlerMethod: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LazyRouteMetadata extends LazyRouteDecoratorConfig {
|
|
9
|
+
path: string;
|
|
10
|
+
method: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function LazyRoute(path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', config: LazyRouteDecoratorConfig): <TFunction extends Function, Y>(target: TFunction | object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LAZY_ROUTE_METADATA = void 0;
|
|
4
|
+
exports.LazyRoute = LazyRoute;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
exports.LAZY_ROUTE_METADATA = 'harpy:lazy-route';
|
|
7
|
+
function LazyRoute(path, method, config) {
|
|
8
|
+
const methodDecorator = method === 'GET'
|
|
9
|
+
? (0, common_1.Get)(path)
|
|
10
|
+
: method === 'POST'
|
|
11
|
+
? (0, common_1.Post)(path)
|
|
12
|
+
: method === 'PUT'
|
|
13
|
+
? (0, common_1.Put)(path)
|
|
14
|
+
: method === 'DELETE'
|
|
15
|
+
? (0, common_1.Delete)(path)
|
|
16
|
+
: (0, common_1.Patch)(path);
|
|
17
|
+
return (0, common_1.applyDecorators)(methodDecorator, (0, common_1.SetMetadata)(exports.LAZY_ROUTE_METADATA, {
|
|
18
|
+
...config,
|
|
19
|
+
path,
|
|
20
|
+
method,
|
|
21
|
+
}));
|
|
22
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export { getChunkPath, getHydrationManifest } from "./core/hydration-manifest";
|
|
|
4
4
|
export { withJsxEngine } from "./core/jsx.engine";
|
|
5
5
|
export { LiveReloadController } from "./core/live-reload.controller";
|
|
6
6
|
export { StaticAssetsController } from "./core/static-assets.controller";
|
|
7
|
+
export { JsxExceptionFilter } from "./core/jsx-exception.filter";
|
|
8
|
+
export type { ErrorPagesConfig } from "./core/jsx-exception.filter";
|
|
9
|
+
export { default as Default404Page } from "./core/error-pages/default-404";
|
|
10
|
+
export { default as Default500Page } from "./core/error-pages/default-500";
|
|
11
|
+
export { default as Default401Page } from "./core/error-pages/default-401";
|
|
12
|
+
export { default as Default403Page } from "./core/error-pages/default-403";
|
|
13
|
+
export { default as ErrorLayout } from "./core/error-pages/error-layout";
|
|
7
14
|
export { JsxRender } from "./decorators/jsx.decorator";
|
|
8
15
|
export { WithLayout } from "./decorators/layout.decorator";
|
|
9
16
|
export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.useI18n = exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
|
|
6
|
+
exports.useI18n = exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = exports.WithLayout = exports.JsxRender = exports.ErrorLayout = exports.Default403Page = exports.Default401Page = exports.Default500Page = exports.Default404Page = exports.JsxExceptionFilter = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
|
|
7
7
|
var client_component_wrapper_1 = require("./core/client-component-wrapper");
|
|
8
8
|
Object.defineProperty(exports, "autoWrapClientComponent", { enumerable: true, get: function () { return client_component_wrapper_1.autoWrapClientComponent; } });
|
|
9
9
|
var hydration_1 = require("./core/hydration");
|
|
@@ -18,6 +18,18 @@ var live_reload_controller_1 = require("./core/live-reload.controller");
|
|
|
18
18
|
Object.defineProperty(exports, "LiveReloadController", { enumerable: true, get: function () { return live_reload_controller_1.LiveReloadController; } });
|
|
19
19
|
var static_assets_controller_1 = require("./core/static-assets.controller");
|
|
20
20
|
Object.defineProperty(exports, "StaticAssetsController", { enumerable: true, get: function () { return static_assets_controller_1.StaticAssetsController; } });
|
|
21
|
+
var jsx_exception_filter_1 = require("./core/jsx-exception.filter");
|
|
22
|
+
Object.defineProperty(exports, "JsxExceptionFilter", { enumerable: true, get: function () { return jsx_exception_filter_1.JsxExceptionFilter; } });
|
|
23
|
+
var default_404_1 = require("./core/error-pages/default-404");
|
|
24
|
+
Object.defineProperty(exports, "Default404Page", { enumerable: true, get: function () { return __importDefault(default_404_1).default; } });
|
|
25
|
+
var default_500_1 = require("./core/error-pages/default-500");
|
|
26
|
+
Object.defineProperty(exports, "Default500Page", { enumerable: true, get: function () { return __importDefault(default_500_1).default; } });
|
|
27
|
+
var default_401_1 = require("./core/error-pages/default-401");
|
|
28
|
+
Object.defineProperty(exports, "Default401Page", { enumerable: true, get: function () { return __importDefault(default_401_1).default; } });
|
|
29
|
+
var default_403_1 = require("./core/error-pages/default-403");
|
|
30
|
+
Object.defineProperty(exports, "Default403Page", { enumerable: true, get: function () { return __importDefault(default_403_1).default; } });
|
|
31
|
+
var error_layout_1 = require("./core/error-pages/error-layout");
|
|
32
|
+
Object.defineProperty(exports, "ErrorLayout", { enumerable: true, get: function () { return __importDefault(error_layout_1).default; } });
|
|
21
33
|
var jsx_decorator_1 = require("./decorators/jsx.decorator");
|
|
22
34
|
Object.defineProperty(exports, "JsxRender", { enumerable: true, get: function () { return jsx_decorator_1.JsxRender; } });
|
|
23
35
|
var layout_decorator_1 = require("./decorators/layout.decorator");
|
package/package.json
CHANGED
package/src/core/app-setup.ts
CHANGED
|
@@ -22,6 +22,12 @@ try {
|
|
|
22
22
|
fastifyCookie = undefined;
|
|
23
23
|
}
|
|
24
24
|
import { withJsxEngine } from "./jsx.engine";
|
|
25
|
+
import { JsxExceptionFilter, ErrorPagesConfig } from "./jsx-exception.filter";
|
|
26
|
+
import { APP_FILTER } from "@nestjs/core";
|
|
27
|
+
import React from "react";
|
|
28
|
+
import { renderToString } from "react-dom/server";
|
|
29
|
+
import Default404Page from "./error-pages/default-404";
|
|
30
|
+
import ErrorLayout from "./error-pages/error-layout";
|
|
25
31
|
|
|
26
32
|
export interface HarpyAppOptions {
|
|
27
33
|
/** JSX Default layout used by the app (optional) */
|
|
@@ -30,6 +36,8 @@ export interface HarpyAppOptions {
|
|
|
30
36
|
distDir?: string;
|
|
31
37
|
/** Optional folder containing public assets (favicon, manifest, etc.) */
|
|
32
38
|
publicDir?: string;
|
|
39
|
+
/** Custom error pages for different HTTP status codes */
|
|
40
|
+
errorPages?: ErrorPagesConfig;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
@@ -45,7 +53,7 @@ export async function configureHarpyApp(
|
|
|
45
53
|
app: NestFastifyApplication,
|
|
46
54
|
opts: HarpyAppOptions = {},
|
|
47
55
|
) {
|
|
48
|
-
const { layout, distDir = "dist", publicDir } = opts;
|
|
56
|
+
const { layout, distDir = "dist", publicDir, errorPages } = opts;
|
|
49
57
|
|
|
50
58
|
if (layout) {
|
|
51
59
|
withJsxEngine(app, layout);
|
|
@@ -53,6 +61,39 @@ export async function configureHarpyApp(
|
|
|
53
61
|
|
|
54
62
|
const fastify = app.getHttpAdapter().getInstance();
|
|
55
63
|
|
|
64
|
+
// Set custom error handler BEFORE other plugins to catch 404s
|
|
65
|
+
// This works with @fastify/static and catches all errors including 404s
|
|
66
|
+
const NotFoundComponent = errorPages?.["404"] || Default404Page;
|
|
67
|
+
fastify.setErrorHandler((error: any, request, reply) => {
|
|
68
|
+
// Check if it's a 404 error
|
|
69
|
+
if (error?.statusCode === 404 || reply.statusCode === 404) {
|
|
70
|
+
try {
|
|
71
|
+
const props = {
|
|
72
|
+
message: "Page Not Found",
|
|
73
|
+
path: request.url,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Wrap the error page content in ErrorLayout for proper styling
|
|
77
|
+
const errorPageContent = React.createElement(NotFoundComponent, props);
|
|
78
|
+
const wrappedInLayout = React.createElement(ErrorLayout, {
|
|
79
|
+
title: "404 - Page Not Found",
|
|
80
|
+
children: errorPageContent,
|
|
81
|
+
});
|
|
82
|
+
const html = renderToString(wrappedInLayout);
|
|
83
|
+
|
|
84
|
+
void reply
|
|
85
|
+
.status(404)
|
|
86
|
+
.header("Content-Type", "text/html; charset=utf-8")
|
|
87
|
+
.send(`<!DOCTYPE html>${html}`);
|
|
88
|
+
return;
|
|
89
|
+
} catch (renderError) {
|
|
90
|
+
console.error("Error rendering 404 page:", renderError);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// For other errors, send them to NestJS exception filters
|
|
94
|
+
throw error;
|
|
95
|
+
});
|
|
96
|
+
|
|
56
97
|
// Cookie support is used by i18n and other helpers if available.
|
|
57
98
|
if (fastifyCookie) {
|
|
58
99
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
@@ -72,21 +113,19 @@ export async function configureHarpyApp(
|
|
|
72
113
|
// This is important: hydration chunks are expected at the root ("/").
|
|
73
114
|
// Use absolute path to be robust when invoked from different CWDs.
|
|
74
115
|
if (fastifyStatic) {
|
|
75
|
-
//
|
|
76
|
-
// This is important: hydration chunks are expected at the root ("/").
|
|
77
|
-
// Use absolute path to be robust when invoked from different CWDs.
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
79
|
-
await fastify.register(fastifyStatic, {
|
|
80
|
-
root: path.join(process.cwd(), distDir),
|
|
81
|
-
prefix: "/",
|
|
82
|
-
decorateReply: false,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// If publicDir is provided, register it as well for public assets
|
|
116
|
+
// If publicDir is provided, register both directories
|
|
86
117
|
if (publicDir) {
|
|
87
118
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
88
119
|
await fastify.register(fastifyStatic, {
|
|
89
|
-
root: path.join(process.cwd(), publicDir),
|
|
120
|
+
root: [path.join(process.cwd(), publicDir), path.join(process.cwd(), distDir)],
|
|
121
|
+
prefix: "/",
|
|
122
|
+
decorateReply: false,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
// Only register dist directory
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
127
|
+
await fastify.register(fastifyStatic, {
|
|
128
|
+
root: path.join(process.cwd(), distDir),
|
|
90
129
|
prefix: "/",
|
|
91
130
|
decorateReply: false,
|
|
92
131
|
});
|
|
@@ -109,6 +148,11 @@ export async function configureHarpyApp(
|
|
|
109
148
|
|
|
110
149
|
// Analytics injection is intentionally omitted — keep analytics opt-in for
|
|
111
150
|
// application authors so they can wire up their provider of choice.
|
|
151
|
+
|
|
152
|
+
// Register global JSX exception filter for custom error pages
|
|
153
|
+
// This must be done via app.useGlobalFilters since we can't modify module providers
|
|
154
|
+
const exceptionFilter = new JsxExceptionFilter(errorPages);
|
|
155
|
+
app.useGlobalFilters(exceptionFilter);
|
|
112
156
|
}
|
|
113
157
|
|
|
114
158
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
|
|
4
|
+
export interface UnauthorizedPageProps extends JsxLayoutProps {
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Default401Page({
|
|
9
|
+
message = 'Unauthorized',
|
|
10
|
+
}: UnauthorizedPageProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-gradient-to-br from-orange-500 to-yellow-400 flex items-center justify-center p-6">
|
|
13
|
+
<div className="max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12">
|
|
14
|
+
<div className="mb-8">
|
|
15
|
+
<div className="inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-orange-500 to-yellow-400 rounded-full shadow-lg mb-6">
|
|
16
|
+
<span className="text-5xl">🔒</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-orange-500 to-yellow-400 mb-4">
|
|
21
|
+
401
|
|
22
|
+
</h1>
|
|
23
|
+
|
|
24
|
+
<h2 className="text-3xl font-bold text-gray-900 mb-4">{message}</h2>
|
|
25
|
+
|
|
26
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
27
|
+
You need to be authenticated to access this resource.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<a
|
|
31
|
+
href="/login"
|
|
32
|
+
className="inline-block px-8 py-3 bg-gradient-to-r from-orange-500 to-yellow-400 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200"
|
|
33
|
+
>
|
|
34
|
+
Go to Login
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
<p className="mt-12 text-sm text-gray-500">
|
|
38
|
+
Powered by <span className="text-orange-600 font-semibold">Harpy.js</span>
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
|
|
4
|
+
export interface ForbiddenPageProps extends JsxLayoutProps {
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Default403Page({
|
|
9
|
+
message = 'Forbidden',
|
|
10
|
+
}: ForbiddenPageProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-gradient-to-br from-red-600 to-orange-500 flex items-center justify-center p-6">
|
|
13
|
+
<div className="max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12">
|
|
14
|
+
<div className="mb-8">
|
|
15
|
+
<div className="inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-red-600 to-orange-500 rounded-full shadow-lg mb-6">
|
|
16
|
+
<span className="text-5xl">⛔</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-red-600 to-orange-500 mb-4">
|
|
21
|
+
403
|
|
22
|
+
</h1>
|
|
23
|
+
|
|
24
|
+
<h2 className="text-3xl font-bold text-gray-900 mb-4">{message}</h2>
|
|
25
|
+
|
|
26
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
27
|
+
You don't have permission to access this resource.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<a
|
|
31
|
+
href="/"
|
|
32
|
+
className="inline-block px-8 py-3 bg-gradient-to-r from-red-600 to-orange-500 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200"
|
|
33
|
+
>
|
|
34
|
+
Go to Homepage
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
<p className="mt-12 text-sm text-gray-500">
|
|
38
|
+
Powered by <span className="text-red-600 font-semibold">Harpy.js</span>
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
|
|
4
|
+
export interface NotFoundPageProps extends JsxLayoutProps {
|
|
5
|
+
path?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Default404Page({
|
|
10
|
+
path,
|
|
11
|
+
message = 'Page Not Found',
|
|
12
|
+
}: NotFoundPageProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="min-h-screen bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center p-6">
|
|
15
|
+
<div className="max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12">
|
|
16
|
+
<div className="mb-8">
|
|
17
|
+
<div className="inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-purple-600 to-blue-600 rounded-full shadow-lg mb-6">
|
|
18
|
+
<span className="text-5xl font-bold text-white">H</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-600 to-blue-600 mb-4">
|
|
23
|
+
404
|
|
24
|
+
</h1>
|
|
25
|
+
|
|
26
|
+
<h2 className="text-3xl font-bold text-gray-900 mb-4">{message}</h2>
|
|
27
|
+
|
|
28
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
29
|
+
The page you're looking for doesn't exist or has been moved.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
{path && (
|
|
33
|
+
<div className="bg-gray-100 border border-gray-300 rounded-lg p-4 mb-8">
|
|
34
|
+
<p className="font-mono text-sm text-gray-700">
|
|
35
|
+
<span className="text-gray-500">Requested path:</span>{' '}
|
|
36
|
+
<span className="text-purple-600 font-semibold">{path}</span>
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
<a
|
|
42
|
+
href="/"
|
|
43
|
+
className="inline-block px-8 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200"
|
|
44
|
+
>
|
|
45
|
+
Go to Homepage
|
|
46
|
+
</a>
|
|
47
|
+
|
|
48
|
+
<p className="mt-12 text-sm text-gray-500">
|
|
49
|
+
Powered by <span className="text-purple-600 font-semibold">Harpy.js</span>
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsxLayoutProps } from '../../types/jsx.types';
|
|
3
|
+
|
|
4
|
+
export interface ServerErrorPageProps extends JsxLayoutProps {
|
|
5
|
+
message?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Default500Page({
|
|
10
|
+
message = 'Internal Server Error',
|
|
11
|
+
error,
|
|
12
|
+
}: ServerErrorPageProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="min-h-screen bg-gradient-to-br from-red-500 to-pink-600 flex items-center justify-center p-6">
|
|
15
|
+
<div className="max-w-2xl w-full text-center bg-white rounded-2xl shadow-2xl p-12">
|
|
16
|
+
<div className="mb-8">
|
|
17
|
+
<div className="inline-flex items-center justify-center w-28 h-28 bg-gradient-to-br from-red-500 to-pink-600 rounded-full shadow-lg mb-6">
|
|
18
|
+
<span className="text-5xl font-bold text-white">H</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-pink-600 mb-4">
|
|
23
|
+
500
|
|
24
|
+
</h1>
|
|
25
|
+
|
|
26
|
+
<h2 className="text-3xl font-bold text-gray-900 mb-4">{message}</h2>
|
|
27
|
+
|
|
28
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
29
|
+
Something went wrong on our end. Please try again later.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
{error && (
|
|
33
|
+
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-6 mb-8 text-left">
|
|
34
|
+
<div className="flex items-start gap-3">
|
|
35
|
+
<svg className="w-6 h-6 text-red-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
36
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
37
|
+
</svg>
|
|
38
|
+
<div className="flex-1">
|
|
39
|
+
<h3 className="text-sm font-semibold text-red-900 mb-2">Error Details</h3>
|
|
40
|
+
<pre className="text-xs text-red-800 font-mono whitespace-pre-wrap break-words">{error}</pre>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<a
|
|
47
|
+
href="/"
|
|
48
|
+
className="inline-block px-8 py-3 bg-gradient-to-r from-red-500 to-pink-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200"
|
|
49
|
+
>
|
|
50
|
+
Go to Homepage
|
|
51
|
+
</a>
|
|
52
|
+
|
|
53
|
+
<p className="mt-12 text-sm text-gray-500">
|
|
54
|
+
Powered by <span className="text-red-600 font-semibold">Harpy.js</span>
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ErrorLayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default layout wrapper for error pages.
|
|
10
|
+
* This ensures styles are loaded and provides a consistent structure.
|
|
11
|
+
*/
|
|
12
|
+
export default function ErrorLayout({
|
|
13
|
+
children,
|
|
14
|
+
title = 'Error',
|
|
15
|
+
}: ErrorLayoutProps) {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charSet="utf-8" />
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
|
+
<title>{title}</title>
|
|
22
|
+
<link rel="stylesheet" href="/styles/styles.css" />
|
|
23
|
+
<link rel="stylesheet" href="/assets/styles.css" />
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
{children}
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExceptionFilter,
|
|
3
|
+
Catch,
|
|
4
|
+
ArgumentsHost,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { renderToString } from 'react-dom/server';
|
|
11
|
+
import Default404Page from './error-pages/default-404';
|
|
12
|
+
import Default500Page from './error-pages/default-500';
|
|
13
|
+
import Default401Page from './error-pages/default-401';
|
|
14
|
+
import Default403Page from './error-pages/default-403';
|
|
15
|
+
import ErrorLayout from './error-pages/error-layout';
|
|
16
|
+
|
|
17
|
+
export interface ErrorPagesConfig {
|
|
18
|
+
/** Custom 404 Not Found page component */
|
|
19
|
+
404?: React.ComponentType<any>;
|
|
20
|
+
/** Custom 500 Internal Server Error page component */
|
|
21
|
+
500?: React.ComponentType<any>;
|
|
22
|
+
/** Custom 401 Unauthorized page component */
|
|
23
|
+
401?: React.ComponentType<any>;
|
|
24
|
+
/** Custom 403 Forbidden page component */
|
|
25
|
+
403?: React.ComponentType<any>;
|
|
26
|
+
/** Generic error page for other status codes */
|
|
27
|
+
default?: React.ComponentType<any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Global exception filter that renders JSX error pages instead of JSON responses.
|
|
32
|
+
* This filter catches all exceptions and renders appropriate error pages based on
|
|
33
|
+
* the HTTP status code.
|
|
34
|
+
*/
|
|
35
|
+
@Catch()
|
|
36
|
+
export class JsxExceptionFilter implements ExceptionFilter {
|
|
37
|
+
constructor(private readonly errorPages: ErrorPagesConfig = {}) {}
|
|
38
|
+
|
|
39
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
40
|
+
const ctx = host.switchToHttp();
|
|
41
|
+
const reply = ctx.getResponse<FastifyReply>();
|
|
42
|
+
const request = ctx.getRequest<FastifyRequest>();
|
|
43
|
+
|
|
44
|
+
// Determine the status code and error message
|
|
45
|
+
let status: number;
|
|
46
|
+
let message: string;
|
|
47
|
+
let error: string | undefined;
|
|
48
|
+
|
|
49
|
+
if (exception instanceof HttpException) {
|
|
50
|
+
status = exception.getStatus();
|
|
51
|
+
const response = exception.getResponse();
|
|
52
|
+
|
|
53
|
+
if (typeof response === 'string') {
|
|
54
|
+
message = response;
|
|
55
|
+
} else if (typeof response === 'object' && response !== null) {
|
|
56
|
+
message =
|
|
57
|
+
(response as any).message || (response as any).error || 'An error occurred';
|
|
58
|
+
error = (response as any).error;
|
|
59
|
+
} else {
|
|
60
|
+
message = 'An error occurred';
|
|
61
|
+
}
|
|
62
|
+
} else if (exception instanceof Error) {
|
|
63
|
+
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
64
|
+
message = 'Internal Server Error';
|
|
65
|
+
error = process.env.NODE_ENV === 'development' ? exception.message : undefined;
|
|
66
|
+
} else {
|
|
67
|
+
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
68
|
+
message = 'Unknown error occurred';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Select the appropriate error page component
|
|
72
|
+
let ErrorComponent: React.ComponentType<any>;
|
|
73
|
+
|
|
74
|
+
switch (status) {
|
|
75
|
+
case HttpStatus.NOT_FOUND:
|
|
76
|
+
ErrorComponent = this.errorPages[404] || Default404Page;
|
|
77
|
+
break;
|
|
78
|
+
case HttpStatus.UNAUTHORIZED:
|
|
79
|
+
ErrorComponent = this.errorPages[401] || Default401Page;
|
|
80
|
+
break;
|
|
81
|
+
case HttpStatus.FORBIDDEN:
|
|
82
|
+
ErrorComponent = this.errorPages[403] || Default403Page;
|
|
83
|
+
break;
|
|
84
|
+
case HttpStatus.INTERNAL_SERVER_ERROR:
|
|
85
|
+
ErrorComponent = this.errorPages[500] || Default500Page;
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
ErrorComponent = this.errorPages.default || this.errorPages[500] || Default500Page;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prepare props for the error page
|
|
92
|
+
const props: any = {
|
|
93
|
+
message,
|
|
94
|
+
error,
|
|
95
|
+
path: request.url,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Determine the appropriate title for the error page
|
|
99
|
+
const titleMap: Record<number, string> = {
|
|
100
|
+
404: '404 - Page Not Found',
|
|
101
|
+
401: '401 - Unauthorized',
|
|
102
|
+
403: '403 - Forbidden',
|
|
103
|
+
500: '500 - Internal Server Error',
|
|
104
|
+
};
|
|
105
|
+
const title = titleMap[status] || `${status} - Error`;
|
|
106
|
+
|
|
107
|
+
// Render the error page wrapped in ErrorLayout for proper styling
|
|
108
|
+
try {
|
|
109
|
+
const errorPageContent = React.createElement(ErrorComponent, props);
|
|
110
|
+
const wrappedInLayout = React.createElement(ErrorLayout, {
|
|
111
|
+
title,
|
|
112
|
+
children: errorPageContent,
|
|
113
|
+
});
|
|
114
|
+
const html = renderToString(wrappedInLayout);
|
|
115
|
+
|
|
116
|
+
void reply
|
|
117
|
+
.status(status)
|
|
118
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
119
|
+
.send(`<!DOCTYPE html>${html}`);
|
|
120
|
+
} catch (renderError) {
|
|
121
|
+
// Fallback to JSON if rendering fails
|
|
122
|
+
console.error('Error rendering JSX error page:', renderError);
|
|
123
|
+
void reply.status(status).send({
|
|
124
|
+
statusCode: status,
|
|
125
|
+
message,
|
|
126
|
+
error,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,15 @@ export { withJsxEngine } from "./core/jsx.engine";
|
|
|
6
6
|
export { LiveReloadController } from "./core/live-reload.controller";
|
|
7
7
|
export { StaticAssetsController } from "./core/static-assets.controller";
|
|
8
8
|
|
|
9
|
+
// Exception filter & error pages
|
|
10
|
+
export { JsxExceptionFilter } from "./core/jsx-exception.filter";
|
|
11
|
+
export type { ErrorPagesConfig } from "./core/jsx-exception.filter";
|
|
12
|
+
export { default as Default404Page } from "./core/error-pages/default-404";
|
|
13
|
+
export { default as Default500Page } from "./core/error-pages/default-500";
|
|
14
|
+
export { default as Default401Page } from "./core/error-pages/default-401";
|
|
15
|
+
export { default as Default403Page } from "./core/error-pages/default-403";
|
|
16
|
+
export { default as ErrorLayout } from "./core/error-pages/error-layout";
|
|
17
|
+
|
|
9
18
|
// Decorators
|
|
10
19
|
export { JsxRender } from "./decorators/jsx.decorator";
|
|
11
20
|
export { WithLayout } from "./decorators/layout.decorator";
|