@harpy-js/core 0.5.5 → 0.5.7
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/__tests__/redirect-logic.spec.d.ts +0 -0
- package/dist/core/__tests__/redirect-logic.spec.js +157 -0
- package/dist/core/app-setup.d.ts +6 -0
- package/dist/core/app-setup.js +66 -1
- 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/__tests__/redirect-logic.spec.ts +200 -0
- package/src/core/app-setup.js.map +1 -0
- package/src/core/app-setup.ts +111 -1
- package/src/core/error-pages/default-401.js.map +1 -0
- package/src/core/error-pages/default-401.tsx +43 -0
- package/src/core/error-pages/default-403.js.map +1 -0
- package/src/core/error-pages/default-403.tsx +43 -0
- package/src/core/error-pages/default-404.js.map +1 -0
- package/src/core/error-pages/default-404.tsx +54 -0
- package/src/core/error-pages/default-500.js.map +1 -0
- package/src/core/error-pages/default-500.tsx +59 -0
- package/src/core/error-pages/error-layout.js.map +1 -0
- package/src/core/error-pages/error-layout.tsx +30 -0
- package/src/core/hydration-manifest.js.map +1 -0
- package/src/core/hydration.js.map +1 -0
- package/src/core/jsx-exception.filter.js.map +1 -0
- package/src/core/jsx-exception.filter.ts +130 -0
- package/src/core/jsx.engine.js.map +1 -0
- package/src/core/live-reload.controller.js.map +1 -0
- package/src/core/static-assets.controller.js.map +1 -0
- package/src/decorators/jsx.decorator.js.map +1 -0
- package/src/index.ts +9 -0
- package/src/types/jsx.types.js.map +1 -0
|
@@ -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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for canonical redirect logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('Canonical Redirect Logic', () => {
|
|
6
|
+
// Simulate the redirect logic from app-setup.ts
|
|
7
|
+
function shouldRedirect(options: {
|
|
8
|
+
host: string;
|
|
9
|
+
protocol: string;
|
|
10
|
+
mainDomain?: string;
|
|
11
|
+
enforceHttps?: boolean;
|
|
12
|
+
redirectWww?: boolean;
|
|
13
|
+
}): { shouldRedirect: boolean; targetUrl?: string } {
|
|
14
|
+
const { host, protocol, mainDomain, enforceHttps = true, redirectWww = true } = options;
|
|
15
|
+
|
|
16
|
+
const normalizedHost = host.replace(/:\d+$/, '');
|
|
17
|
+
|
|
18
|
+
// Skip redirects for localhost/127.0.0.1
|
|
19
|
+
if (normalizedHost === 'localhost' || normalizedHost === '127.0.0.1' || normalizedHost === '0.0.0.0') {
|
|
20
|
+
return { shouldRedirect: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const proto = protocol;
|
|
24
|
+
|
|
25
|
+
// Decide whether to redirect
|
|
26
|
+
const isHttp = enforceHttps && proto !== 'https';
|
|
27
|
+
const isWww = redirectWww && normalizedHost.startsWith('www.');
|
|
28
|
+
const hostMismatch = mainDomain && normalizedHost !== mainDomain && normalizedHost !== `www.${mainDomain}`;
|
|
29
|
+
|
|
30
|
+
if (isHttp || isWww || hostMismatch) {
|
|
31
|
+
const targetHost = mainDomain ? mainDomain : normalizedHost.replace(/^www\./, '');
|
|
32
|
+
const targetProto = enforceHttps ? 'https' : proto;
|
|
33
|
+
const targetUrl = `${targetProto}://${targetHost}/`;
|
|
34
|
+
|
|
35
|
+
return { shouldRedirect: true, targetUrl };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { shouldRedirect: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('HTTP to HTTPS redirect', () => {
|
|
42
|
+
it('should redirect http://harpyjs.org to https://harpyjs.org', () => {
|
|
43
|
+
const result = shouldRedirect({
|
|
44
|
+
host: 'harpyjs.org',
|
|
45
|
+
protocol: 'http',
|
|
46
|
+
mainDomain: 'harpyjs.org',
|
|
47
|
+
enforceHttps: true,
|
|
48
|
+
redirectWww: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.shouldRedirect).toBe(true);
|
|
52
|
+
expect(result.targetUrl).toBe('https://harpyjs.org/');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not redirect https://harpyjs.org', () => {
|
|
56
|
+
const result = shouldRedirect({
|
|
57
|
+
host: 'harpyjs.org',
|
|
58
|
+
protocol: 'https',
|
|
59
|
+
mainDomain: 'harpyjs.org',
|
|
60
|
+
enforceHttps: true,
|
|
61
|
+
redirectWww: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.shouldRedirect).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('WWW to non-WWW redirect', () => {
|
|
69
|
+
it('should redirect www.harpyjs.org to harpyjs.org', () => {
|
|
70
|
+
const result = shouldRedirect({
|
|
71
|
+
host: 'www.harpyjs.org',
|
|
72
|
+
protocol: 'https',
|
|
73
|
+
mainDomain: 'harpyjs.org',
|
|
74
|
+
enforceHttps: true,
|
|
75
|
+
redirectWww: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.shouldRedirect).toBe(true);
|
|
79
|
+
expect(result.targetUrl).toBe('https://harpyjs.org/');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should not redirect when redirectWww is false', () => {
|
|
83
|
+
const result = shouldRedirect({
|
|
84
|
+
host: 'www.harpyjs.org',
|
|
85
|
+
protocol: 'https',
|
|
86
|
+
mainDomain: 'harpyjs.org',
|
|
87
|
+
enforceHttps: true,
|
|
88
|
+
redirectWww: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.shouldRedirect).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Combined redirects', () => {
|
|
96
|
+
it('should redirect http://www.harpyjs.org to https://harpyjs.org', () => {
|
|
97
|
+
const result = shouldRedirect({
|
|
98
|
+
host: 'www.harpyjs.org',
|
|
99
|
+
protocol: 'http',
|
|
100
|
+
mainDomain: 'harpyjs.org',
|
|
101
|
+
enforceHttps: true,
|
|
102
|
+
redirectWww: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.shouldRedirect).toBe(true);
|
|
106
|
+
expect(result.targetUrl).toBe('https://harpyjs.org/');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Localhost exemption', () => {
|
|
111
|
+
it('should not redirect localhost', () => {
|
|
112
|
+
const result = shouldRedirect({
|
|
113
|
+
host: 'localhost',
|
|
114
|
+
protocol: 'http',
|
|
115
|
+
mainDomain: 'harpyjs.org',
|
|
116
|
+
enforceHttps: true,
|
|
117
|
+
redirectWww: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.shouldRedirect).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should not redirect 127.0.0.1', () => {
|
|
124
|
+
const result = shouldRedirect({
|
|
125
|
+
host: '127.0.0.1',
|
|
126
|
+
protocol: 'http',
|
|
127
|
+
mainDomain: 'harpyjs.org',
|
|
128
|
+
enforceHttps: true,
|
|
129
|
+
redirectWww: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.shouldRedirect).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not redirect localhost:3000', () => {
|
|
136
|
+
const result = shouldRedirect({
|
|
137
|
+
host: 'localhost:3000',
|
|
138
|
+
protocol: 'http',
|
|
139
|
+
mainDomain: 'harpyjs.org',
|
|
140
|
+
enforceHttps: true,
|
|
141
|
+
redirectWww: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.shouldRedirect).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Domain enforcement', () => {
|
|
149
|
+
it('should redirect different domain to mainDomain', () => {
|
|
150
|
+
const result = shouldRedirect({
|
|
151
|
+
host: 'example.com',
|
|
152
|
+
protocol: 'https',
|
|
153
|
+
mainDomain: 'harpyjs.org',
|
|
154
|
+
enforceHttps: true,
|
|
155
|
+
redirectWww: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.shouldRedirect).toBe(true);
|
|
159
|
+
expect(result.targetUrl).toBe('https://harpyjs.org/');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should not redirect when host matches mainDomain', () => {
|
|
163
|
+
const result = shouldRedirect({
|
|
164
|
+
host: 'harpyjs.org',
|
|
165
|
+
protocol: 'https',
|
|
166
|
+
mainDomain: 'harpyjs.org',
|
|
167
|
+
enforceHttps: true,
|
|
168
|
+
redirectWww: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.shouldRedirect).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Configuration variations', () => {
|
|
176
|
+
it('should not redirect http when enforceHttps is false', () => {
|
|
177
|
+
const result = shouldRedirect({
|
|
178
|
+
host: 'harpyjs.org',
|
|
179
|
+
protocol: 'http',
|
|
180
|
+
mainDomain: 'harpyjs.org',
|
|
181
|
+
enforceHttps: false,
|
|
182
|
+
redirectWww: true,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result.shouldRedirect).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should work without mainDomain specified', () => {
|
|
189
|
+
const result = shouldRedirect({
|
|
190
|
+
host: 'www.example.com',
|
|
191
|
+
protocol: 'https',
|
|
192
|
+
enforceHttps: true,
|
|
193
|
+
redirectWww: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.shouldRedirect).toBe(true);
|
|
197
|
+
expect(result.targetUrl).toBe('https://example.com/');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-setup.js","sourceRoot":"","sources":["app-setup.ts"],"names":[],"mappings":"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"}
|
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,18 @@ 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;
|
|
41
|
+
/**
|
|
42
|
+
* Optional redirect settings. When `enforceRedirects` is true (default false)
|
|
43
|
+
* Harpy will register a Fastify `onRequest` hook to redirect requests to
|
|
44
|
+
* the canonical domain and HTTPS. Configure `mainDomain` to your primary
|
|
45
|
+
* host (e.g. `harpyjs.org`).
|
|
46
|
+
*/
|
|
47
|
+
enforceRedirects?: boolean;
|
|
48
|
+
mainDomain?: string;
|
|
49
|
+
enforceHttps?: boolean;
|
|
50
|
+
redirectWww?: boolean;
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/**
|
|
@@ -45,7 +63,7 @@ export async function configureHarpyApp(
|
|
|
45
63
|
app: NestFastifyApplication,
|
|
46
64
|
opts: HarpyAppOptions = {},
|
|
47
65
|
) {
|
|
48
|
-
const { layout, distDir = "dist", publicDir } = opts;
|
|
66
|
+
const { layout, distDir = "dist", publicDir, errorPages } = opts;
|
|
49
67
|
|
|
50
68
|
if (layout) {
|
|
51
69
|
withJsxEngine(app, layout);
|
|
@@ -53,6 +71,93 @@ export async function configureHarpyApp(
|
|
|
53
71
|
|
|
54
72
|
const fastify = app.getHttpAdapter().getInstance();
|
|
55
73
|
|
|
74
|
+
// Optional redirects to canonical domain / HTTPS
|
|
75
|
+
const {
|
|
76
|
+
enforceRedirects = false,
|
|
77
|
+
mainDomain,
|
|
78
|
+
enforceHttps = true,
|
|
79
|
+
redirectWww = true,
|
|
80
|
+
} = opts as HarpyAppOptions;
|
|
81
|
+
|
|
82
|
+
if (enforceRedirects && (mainDomain || enforceHttps || redirectWww)) {
|
|
83
|
+
// Register early hook to redirect incoming requests to the canonical URL
|
|
84
|
+
// This keeps redirect handling in Harpy core so consumers can opt-in.
|
|
85
|
+
// Uses x-forwarded-proto when behind proxies (Vercel, Cloudflare)
|
|
86
|
+
// and falls back to socket encryption detection.
|
|
87
|
+
// NOTE: Make sure your proxy forwards `x-forwarded-proto`.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
fastify.addHook('onRequest', (req: any, reply: any, done: any) => {
|
|
90
|
+
try {
|
|
91
|
+
const hostHeader = (req.headers && (req.headers.host || '')).toString();
|
|
92
|
+
const normalizedHost = hostHeader.replace(/:\d+$/, '');
|
|
93
|
+
|
|
94
|
+
// Skip redirects for localhost/127.0.0.1 (local development)
|
|
95
|
+
if (normalizedHost === 'localhost' || normalizedHost === '127.0.0.1' || normalizedHost === '0.0.0.0') {
|
|
96
|
+
done();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const forwardedProto = (req.headers['x-forwarded-proto'] || '').toString();
|
|
101
|
+
const proto = forwardedProto || (req.raw && req.raw.socket && (req.raw.socket as any).encrypted ? 'https' : 'http');
|
|
102
|
+
|
|
103
|
+
// Decide whether to redirect: http -> https, www -> apex, or host mismatch
|
|
104
|
+
const isHttp = enforceHttps && proto !== 'https';
|
|
105
|
+
const isWww = redirectWww && normalizedHost.startsWith('www.');
|
|
106
|
+
const hostMismatch = mainDomain && normalizedHost !== mainDomain && normalizedHost !== `www.${mainDomain}`;
|
|
107
|
+
|
|
108
|
+
if (isHttp || isWww || hostMismatch) {
|
|
109
|
+
// Build target host (prefer configured mainDomain, else strip www.)
|
|
110
|
+
const targetHost = mainDomain ? mainDomain : normalizedHost.replace(/^www\./, '');
|
|
111
|
+
const targetProto = enforceHttps ? 'https' : proto;
|
|
112
|
+
const targetUrl = `${targetProto}://${targetHost}${req.url}`;
|
|
113
|
+
|
|
114
|
+
// Permanent redirect
|
|
115
|
+
reply.status(301).header('Cache-Control', 'public, max-age=31536000').redirect(targetUrl);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// swallow errors and continue to avoid blocking requests
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.warn('[harpy-core] redirect hook error', e);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
done();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Set custom error handler BEFORE other plugins to catch 404s
|
|
129
|
+
// This works with @fastify/static and catches all errors including 404s
|
|
130
|
+
const NotFoundComponent = errorPages?.["404"] || Default404Page;
|
|
131
|
+
fastify.setErrorHandler((error: any, request, reply) => {
|
|
132
|
+
// Check if it's a 404 error
|
|
133
|
+
if (error?.statusCode === 404 || reply.statusCode === 404) {
|
|
134
|
+
try {
|
|
135
|
+
const props = {
|
|
136
|
+
message: "Page Not Found",
|
|
137
|
+
path: request.url,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Wrap the error page content in ErrorLayout for proper styling
|
|
141
|
+
const errorPageContent = React.createElement(NotFoundComponent, props);
|
|
142
|
+
const wrappedInLayout = React.createElement(ErrorLayout, {
|
|
143
|
+
title: "404 - Page Not Found",
|
|
144
|
+
children: errorPageContent,
|
|
145
|
+
});
|
|
146
|
+
const html = renderToString(wrappedInLayout);
|
|
147
|
+
|
|
148
|
+
void reply
|
|
149
|
+
.status(404)
|
|
150
|
+
.header("Content-Type", "text/html; charset=utf-8")
|
|
151
|
+
.send(`<!DOCTYPE html>${html}`);
|
|
152
|
+
return;
|
|
153
|
+
} catch (renderError) {
|
|
154
|
+
console.error("Error rendering 404 page:", renderError);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// For other errors, send them to NestJS exception filters
|
|
158
|
+
throw error;
|
|
159
|
+
});
|
|
160
|
+
|
|
56
161
|
// Cookie support is used by i18n and other helpers if available.
|
|
57
162
|
if (fastifyCookie) {
|
|
58
163
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
@@ -107,6 +212,11 @@ export async function configureHarpyApp(
|
|
|
107
212
|
|
|
108
213
|
// Analytics injection is intentionally omitted — keep analytics opt-in for
|
|
109
214
|
// application authors so they can wire up their provider of choice.
|
|
215
|
+
|
|
216
|
+
// Register global JSX exception filter for custom error pages
|
|
217
|
+
// This must be done via app.useGlobalFilters since we can't modify module providers
|
|
218
|
+
const exceptionFilter = new JsxExceptionFilter(errorPages);
|
|
219
|
+
app.useGlobalFilters(exceptionFilter);
|
|
110
220
|
}
|
|
111
221
|
|
|
112
222
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-401.js","sourceRoot":"","sources":["default-401.tsx"],"names":[],"mappings":";;AAOA,iCAmCC;;AAnCD,SAAwB,cAAc,CAAC,EACrC,OAAO,GAAG,cAAc,GACF;IACtB,OAAO,CACL,gCAAK,SAAS,EAAC,mGAAmG,YAChH,iCAAK,SAAS,EAAC,mEAAmE,aAChF,gCAAK,SAAS,EAAC,MAAM,YACnB,gCAAK,SAAS,EAAC,+HAA+H,YAC5I,iCAAM,SAAS,EAAC,UAAU,6BAAU,GAChC,GACF,EAEN,+BAAI,SAAS,EAAC,2GAA2G,oBAEpH,EAEL,+BAAI,SAAS,EAAC,uCAAuC,YAAE,OAAO,GAAM,EAEpE,8BAAG,SAAS,EAAC,4BAA4B,sEAErC,EAEJ,8BACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,kMAAkM,4BAG1M,EAEJ,+BAAG,SAAS,EAAC,6BAA6B,4BAC7B,iCAAM,SAAS,EAAC,+BAA+B,yBAAgB,IACxE,IACA,GACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"default-403.js","sourceRoot":"","sources":["default-403.tsx"],"names":[],"mappings":";;AAOA,iCAmCC;;AAnCD,SAAwB,cAAc,CAAC,EACrC,OAAO,GAAG,WAAW,GACF;IACnB,OAAO,CACL,gCAAK,SAAS,EAAC,gGAAgG,YAC7G,iCAAK,SAAS,EAAC,mEAAmE,aAChF,gCAAK,SAAS,EAAC,MAAM,YACnB,gCAAK,SAAS,EAAC,4HAA4H,YACzI,iCAAM,SAAS,EAAC,UAAU,uBAAS,GAC/B,GACF,EAEN,+BAAI,SAAS,EAAC,wGAAwG,oBAEjH,EAEL,+BAAI,SAAS,EAAC,uCAAuC,YAAE,OAAO,GAAM,EAEpE,8BAAG,SAAS,EAAC,4BAA4B,mEAErC,EAEJ,8BACE,IAAI,EAAC,GAAG,EACR,SAAS,EAAC,+LAA+L,+BAGvM,EAEJ,+BAAG,SAAS,EAAC,6BAA6B,4BAC7B,iCAAM,SAAS,EAAC,4BAA4B,yBAAgB,IACrE,IACA,GACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"default-404.js","sourceRoot":"","sources":["default-404.tsx"],"names":[],"mappings":";;AAQA,iCA6CC;;AA7CD,SAAwB,cAAc,CAAC,EACrC,IAAI,EACJ,OAAO,GAAG,gBAAgB,GACR;IAClB,OAAO,CACL,gCAAK,SAAS,EAAC,iGAAiG,YAC9G,iCAAK,SAAS,EAAC,mEAAmE,aAChF,gCAAK,SAAS,EAAC,MAAM,YACnB,gCAAK,SAAS,EAAC,6HAA6H,YAC1I,iCAAM,SAAS,EAAC,+BAA+B,kBAAS,GACpD,GACF,EAEN,+BAAI,SAAS,EAAC,yGAAyG,oBAElH,EAEL,+BAAI,SAAS,EAAC,uCAAuC,YAAE,OAAO,GAAM,EAEpE,8BAAG,SAAS,EAAC,4BAA4B,6EAErC,EAEH,IAAI,IAAI,CACP,gCAAK,SAAS,EAAC,wDAAwD,YACrE,+BAAG,SAAS,EAAC,iCAAiC,aAC5C,iCAAM,SAAS,EAAC,eAAe,gCAAuB,EAAC,GAAG,EAC1D,iCAAM,SAAS,EAAC,+BAA+B,YAAE,IAAI,GAAQ,IAC3D,GACA,CACP,EAED,8BACE,IAAI,EAAC,GAAG,EACR,SAAS,EAAC,gMAAgM,+BAGxM,EAEJ,+BAAG,SAAS,EAAC,6BAA6B,4BAC7B,iCAAM,SAAS,EAAC,+BAA+B,yBAAgB,IACxE,IACA,GACF,CACP,CAAC;AACJ,CAAC"}
|