@harpy-js/core 0.4.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/README.md +326 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +53 -0
- package/dist/client/Link.d.ts +5 -0
- package/dist/client/Link.js +62 -0
- package/dist/client/__tests__/getActiveItemId.test.d.ts +1 -0
- package/dist/client/__tests__/getActiveItemId.test.js +38 -0
- package/dist/client/getActiveItemId.d.ts +7 -0
- package/dist/client/getActiveItemId.js +55 -0
- package/dist/client/use-i18n.d.ts +7 -0
- package/dist/client/use-i18n.js +64 -0
- package/dist/core/__tests__/component-analyzer.test.d.ts +1 -0
- package/dist/core/__tests__/component-analyzer.test.js +151 -0
- package/dist/core/__tests__/hydration-manifest.test.d.ts +1 -0
- package/dist/core/__tests__/hydration-manifest.test.js +211 -0
- package/dist/core/__tests__/jsx.engine.test.d.ts +1 -0
- package/dist/core/__tests__/jsx.engine.test.js +118 -0
- package/dist/core/app-setup.d.ts +7 -0
- package/dist/core/app-setup.js +79 -0
- package/dist/core/auto-register.module.d.ts +9 -0
- package/dist/core/auto-register.module.js +18 -0
- package/dist/core/auto-wrap-middleware.d.ts +4 -0
- package/dist/core/auto-wrap-middleware.js +130 -0
- package/dist/core/client-component-wrapper.d.ts +5 -0
- package/dist/core/client-component-wrapper.js +37 -0
- package/dist/core/client-hydration.d.ts +2 -0
- package/dist/core/client-hydration.js +93 -0
- package/dist/core/client-wrapper-browser.d.ts +2 -0
- package/dist/core/client-wrapper-browser.js +22 -0
- package/dist/core/component-analyzer.d.ts +4 -0
- package/dist/core/component-analyzer.js +98 -0
- package/dist/core/component-auto-wrapper.d.ts +2 -0
- package/dist/core/component-auto-wrapper.js +63 -0
- package/dist/core/component-client-wrapper.d.ts +4 -0
- package/dist/core/component-client-wrapper.js +80 -0
- package/dist/core/hydration-generator.d.ts +2 -0
- package/dist/core/hydration-generator.js +98 -0
- package/dist/core/hydration-manifest.d.ts +7 -0
- package/dist/core/hydration-manifest.js +83 -0
- package/dist/core/hydration.d.ts +16 -0
- package/dist/core/hydration.js +72 -0
- package/dist/core/jsx.engine.d.ts +9 -0
- package/dist/core/jsx.engine.js +161 -0
- package/dist/core/live-reload-client.js +32 -0
- package/dist/core/live-reload.controller.d.ts +10 -0
- package/dist/core/live-reload.controller.js +38 -0
- package/dist/core/navigation.service.d.ts +18 -0
- package/dist/core/navigation.service.js +206 -0
- package/dist/core/router.module.d.ts +2 -0
- package/dist/core/router.module.js +21 -0
- package/dist/core/static-assets.controller.d.ts +4 -0
- package/dist/core/static-assets.controller.js +51 -0
- package/dist/core/types/nav.types.d.ts +22 -0
- package/dist/core/types/nav.types.js +2 -0
- package/dist/core/views/layout.d.ts +8 -0
- package/dist/core/views/layout.js +35 -0
- package/dist/decorators/jsx.decorator.d.ts +26 -0
- package/dist/decorators/jsx.decorator.js +10 -0
- package/dist/decorators/layout.decorator.d.ts +4 -0
- package/dist/decorators/layout.decorator.js +29 -0
- package/dist/i18n/__tests__/i18n.helper.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.helper.test.js +105 -0
- package/dist/i18n/__tests__/i18n.interceptor.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.interceptor.test.js +195 -0
- package/dist/i18n/__tests__/i18n.module.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.module.test.js +83 -0
- package/dist/i18n/__tests__/i18n.service.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.service.test.js +109 -0
- package/dist/i18n/__tests__/t.test.d.ts +1 -0
- package/dist/i18n/__tests__/t.test.js +66 -0
- package/dist/i18n/i18n-module.options.d.ts +10 -0
- package/dist/i18n/i18n-module.options.js +4 -0
- package/dist/i18n/i18n-switcher.controller.d.ts +12 -0
- package/dist/i18n/i18n-switcher.controller.js +80 -0
- package/dist/i18n/i18n-types.d.ts +8 -0
- package/dist/i18n/i18n-types.js +2 -0
- package/dist/i18n/i18n.helper.d.ts +14 -0
- package/dist/i18n/i18n.helper.js +70 -0
- package/dist/i18n/i18n.interceptor.d.ts +9 -0
- package/dist/i18n/i18n.interceptor.js +99 -0
- package/dist/i18n/i18n.module.d.ts +5 -0
- package/dist/i18n/i18n.module.js +51 -0
- package/dist/i18n/i18n.service.d.ts +12 -0
- package/dist/i18n/i18n.service.js +61 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.js +20 -0
- package/dist/i18n/locale.decorator.d.ts +1 -0
- package/dist/i18n/locale.decorator.js +8 -0
- package/dist/i18n/t.d.ts +3 -0
- package/dist/i18n/t.js +16 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +40 -0
- package/package.json +79 -0
- package/scripts/analyze-styles.ts +124 -0
- package/scripts/auto-wrap-exports.ts +239 -0
- package/scripts/build-css.ts +38 -0
- package/scripts/build-hydration.ts +313 -0
- package/scripts/build-page-styles.ts +43 -0
- package/scripts/copy-assets.ts +34 -0
- package/scripts/dev.sh +3 -0
- package/scripts/dev.ts +257 -0
- package/src/cli.ts +71 -0
- package/src/client/Link.tsx +62 -0
- package/src/client/__tests__/getActiveItemId.test.ts +49 -0
- package/src/client/getActiveItemId.ts +54 -0
- package/src/client/use-i18n.ts +111 -0
- package/src/core/__tests__/component-analyzer.test.ts +141 -0
- package/src/core/__tests__/hydration-manifest.test.ts +223 -0
- package/src/core/__tests__/jsx.engine.test.ts +137 -0
- package/src/core/app-setup.ts +114 -0
- package/src/core/auto-register.module.ts +30 -0
- package/src/core/auto-wrap-middleware.ts +165 -0
- package/src/core/client-component-wrapper.ts +72 -0
- package/src/core/client-hydration.tsx +99 -0
- package/src/core/client-wrapper-browser.ts +40 -0
- package/src/core/component-analyzer.ts +89 -0
- package/src/core/component-auto-wrapper.ts +68 -0
- package/src/core/component-client-wrapper.ts +112 -0
- package/src/core/hydration-generator.ts +94 -0
- package/src/core/hydration-manifest.ts +79 -0
- package/src/core/hydration.ts +70 -0
- package/src/core/jsx.engine.ts +205 -0
- package/src/core/live-reload-client.js +32 -0
- package/src/core/live-reload.controller.ts +55 -0
- package/src/core/navigation.service.ts +257 -0
- package/src/core/router.module.ts +9 -0
- package/src/core/static-assets.controller.ts +19 -0
- package/src/core/types/nav.types.ts +53 -0
- package/src/core/views/layout.tsx +61 -0
- package/src/decorators/jsx.decorator.ts +49 -0
- package/src/decorators/layout.decorator.ts +66 -0
- package/src/i18n/__tests__/i18n.helper.test.ts +126 -0
- package/src/i18n/__tests__/i18n.interceptor.test.ts +229 -0
- package/src/i18n/__tests__/i18n.module.test.ts +98 -0
- package/src/i18n/__tests__/i18n.service.test.ts +129 -0
- package/src/i18n/__tests__/t.test.ts +88 -0
- package/src/i18n/i18n-module.options.ts +53 -0
- package/src/i18n/i18n-switcher.controller.ts +99 -0
- package/src/i18n/i18n-types.ts +56 -0
- package/src/i18n/i18n.helper.ts +75 -0
- package/src/i18n/i18n.interceptor.ts +114 -0
- package/src/i18n/i18n.module.ts +45 -0
- package/src/i18n/i18n.service.ts +95 -0
- package/src/i18n/index.ts +37 -0
- package/src/i18n/locale.decorator.ts +10 -0
- package/src/i18n/t.ts +62 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.StaticAssetsController = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class StaticAssetsController {
|
|
40
|
+
liveReloadScript(reply) {
|
|
41
|
+
const scriptPath = path.join(__dirname, "live-reload-client.js");
|
|
42
|
+
if (fs.existsSync(scriptPath)) {
|
|
43
|
+
const script = fs.readFileSync(scriptPath, "utf-8");
|
|
44
|
+
reply.type("application/javascript").send(script);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
reply.code(404).send("Live reload script not found");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.StaticAssetsController = StaticAssetsController;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface NavItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
order?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface NavSection {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
items: NavItem[];
|
|
12
|
+
order?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface NavigationRegistry {
|
|
15
|
+
registerSection(section: NavSection): void;
|
|
16
|
+
addItemToSection(sectionId: string, item: NavItem): void;
|
|
17
|
+
registerItem(item: NavItem): void;
|
|
18
|
+
getSectionsForRoute(currentPath?: string): NavSection[];
|
|
19
|
+
getActiveItemId(currentPath?: string): string | undefined;
|
|
20
|
+
getAllSections(): NavSection[];
|
|
21
|
+
getSection(sectionId: string): NavSection | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { JsxLayoutProps } from "../jsx.engine";
|
|
3
|
+
export default function DefaultLayout({ children, meta, hydrationScripts, }: JsxLayoutProps & {
|
|
4
|
+
hydrationScripts?: Array<{
|
|
5
|
+
componentName: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}>;
|
|
8
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
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 = DefaultLayout;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
function DefaultLayout({ children, meta, hydrationScripts, }) {
|
|
9
|
+
const title = meta?.title ?? "My App";
|
|
10
|
+
const description = meta?.description ?? "Default app description";
|
|
11
|
+
const canonical = meta?.canonical ?? "https://example.com";
|
|
12
|
+
const og = meta?.openGraph ?? {};
|
|
13
|
+
const twitter = meta?.twitter ?? {};
|
|
14
|
+
const chunkScripts = hydrationScripts || [];
|
|
15
|
+
return (react_1.default.createElement("html", { lang: "en" },
|
|
16
|
+
react_1.default.createElement("head", null,
|
|
17
|
+
react_1.default.createElement("title", null, title),
|
|
18
|
+
react_1.default.createElement("meta", { charSet: "utf-8" }),
|
|
19
|
+
react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
20
|
+
react_1.default.createElement("link", { rel: "stylesheet", href: "/styles/styles.css" }),
|
|
21
|
+
react_1.default.createElement("meta", { name: "description", content: description }),
|
|
22
|
+
react_1.default.createElement("link", { rel: "canonical", href: canonical }),
|
|
23
|
+
og.title && react_1.default.createElement("meta", { property: "og:title", content: og.title }),
|
|
24
|
+
og.description && (react_1.default.createElement("meta", { property: "og:description", content: og.description })),
|
|
25
|
+
og.type && react_1.default.createElement("meta", { property: "og:type", content: og.type }),
|
|
26
|
+
og.image && react_1.default.createElement("meta", { property: "og:image", content: og.image }),
|
|
27
|
+
og.url && react_1.default.createElement("meta", { property: "og:url", content: og.url }),
|
|
28
|
+
twitter.card && react_1.default.createElement("meta", { name: "twitter:card", content: twitter.card }),
|
|
29
|
+
twitter.title && react_1.default.createElement("meta", { name: "twitter:title", content: twitter.title }),
|
|
30
|
+
twitter.description && (react_1.default.createElement("meta", { name: "twitter:description", content: twitter.description })),
|
|
31
|
+
twitter.image && react_1.default.createElement("meta", { name: "twitter:image", content: twitter.image })),
|
|
32
|
+
react_1.default.createElement("body", { className: "bg-slate-50" },
|
|
33
|
+
react_1.default.createElement("main", { id: "body", className: "min-h-screen" }, children),
|
|
34
|
+
chunkScripts.map((script) => (react_1.default.createElement("script", { key: script.componentName, src: script.path }))))));
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JsxLayout } from "../core/jsx.engine";
|
|
2
|
+
export interface MetaOptions {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
canonical?: string;
|
|
6
|
+
openGraph?: {
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
image?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
};
|
|
13
|
+
twitter?: {
|
|
14
|
+
card?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface RenderOptions {
|
|
21
|
+
layout?: JsxLayout;
|
|
22
|
+
bootstrapScripts?: string[];
|
|
23
|
+
meta?: MetaResolver;
|
|
24
|
+
}
|
|
25
|
+
export type MetaResolver<T = any> = MetaOptions | ((req: any, data: T) => MetaOptions | Promise<MetaOptions>);
|
|
26
|
+
export declare function JsxRender<T>(template: (data: T) => React.JSX.Element, options?: RenderOptions): MethodDecorator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsxRender = JsxRender;
|
|
4
|
+
const constants_1 = require("@nestjs/common/constants");
|
|
5
|
+
function JsxRender(template, options = {}) {
|
|
6
|
+
return (target, key, descriptor) => {
|
|
7
|
+
Reflect.defineMetadata(constants_1.RENDER_METADATA, [template, options], descriptor.value);
|
|
8
|
+
return descriptor;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { JsxLayout } from "../core/jsx.engine";
|
|
2
|
+
export declare const LAYOUT_METADATA = "harpy:layout";
|
|
3
|
+
export declare function WithLayout(layout: JsxLayout): ClassDecorator & MethodDecorator;
|
|
4
|
+
export declare function getLayoutForHandler(handler: Function, controllerClass: Function): JsxLayout | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LAYOUT_METADATA = void 0;
|
|
4
|
+
exports.WithLayout = WithLayout;
|
|
5
|
+
exports.getLayoutForHandler = getLayoutForHandler;
|
|
6
|
+
exports.LAYOUT_METADATA = "harpy:layout";
|
|
7
|
+
function WithLayout(layout) {
|
|
8
|
+
return (target, propertyKey, descriptor) => {
|
|
9
|
+
if (propertyKey && descriptor) {
|
|
10
|
+
Reflect.defineMetadata(exports.LAYOUT_METADATA, layout, descriptor.value);
|
|
11
|
+
return descriptor;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
Reflect.defineMetadata(exports.LAYOUT_METADATA, layout, target);
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function getLayoutForHandler(handler, controllerClass) {
|
|
20
|
+
const methodLayout = Reflect.getMetadata(exports.LAYOUT_METADATA, handler);
|
|
21
|
+
if (methodLayout) {
|
|
22
|
+
return methodLayout;
|
|
23
|
+
}
|
|
24
|
+
const controllerLayout = Reflect.getMetadata(exports.LAYOUT_METADATA, controllerClass);
|
|
25
|
+
if (controllerLayout) {
|
|
26
|
+
return controllerLayout;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const testing_1 = require("@nestjs/testing");
|
|
4
|
+
const i18n_helper_1 = require("../i18n.helper");
|
|
5
|
+
const i18n_module_options_1 = require("../i18n-module.options");
|
|
6
|
+
describe("I18nHelper", () => {
|
|
7
|
+
let helper;
|
|
8
|
+
const mockOptions = {
|
|
9
|
+
defaultLocale: "en",
|
|
10
|
+
locales: ["en", "fr", "es"],
|
|
11
|
+
urlPattern: "query",
|
|
12
|
+
translationsPath: "./dictionaries",
|
|
13
|
+
cookieName: "locale",
|
|
14
|
+
queryParam: "lang",
|
|
15
|
+
};
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
const module = await testing_1.Test.createTestingModule({
|
|
18
|
+
providers: [
|
|
19
|
+
i18n_helper_1.I18nHelper,
|
|
20
|
+
{
|
|
21
|
+
provide: i18n_module_options_1.I18N_MODULE_OPTIONS,
|
|
22
|
+
useValue: mockOptions,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}).compile();
|
|
26
|
+
helper = module.get(i18n_helper_1.I18nHelper);
|
|
27
|
+
});
|
|
28
|
+
describe("buildLocaleUrl", () => {
|
|
29
|
+
describe("query pattern", () => {
|
|
30
|
+
it("should build URL with query parameter", () => {
|
|
31
|
+
const url = helper.buildLocaleUrl("fr", "/home");
|
|
32
|
+
expect(url).toBe("/home?lang=fr");
|
|
33
|
+
});
|
|
34
|
+
it("should preserve existing query parameters", () => {
|
|
35
|
+
const url = helper.buildLocaleUrl("fr", "/home", { page: "2" });
|
|
36
|
+
expect(url).toContain("lang=fr");
|
|
37
|
+
expect(url).toContain("page=2");
|
|
38
|
+
});
|
|
39
|
+
it("should replace existing lang parameter", () => {
|
|
40
|
+
const url = helper.buildLocaleUrl("fr", "/home", { lang: "en" });
|
|
41
|
+
expect(url).toBe("/home?lang=fr");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("header pattern", () => {
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
const module = await testing_1.Test.createTestingModule({
|
|
47
|
+
providers: [
|
|
48
|
+
i18n_helper_1.I18nHelper,
|
|
49
|
+
{
|
|
50
|
+
provide: i18n_module_options_1.I18N_MODULE_OPTIONS,
|
|
51
|
+
useValue: { ...mockOptions, urlPattern: "header" },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}).compile();
|
|
55
|
+
helper = module.get(i18n_helper_1.I18nHelper);
|
|
56
|
+
});
|
|
57
|
+
it("should return path without query for header pattern", () => {
|
|
58
|
+
const url = helper.buildLocaleUrl("fr", "/home");
|
|
59
|
+
expect(url).toBe("/home");
|
|
60
|
+
});
|
|
61
|
+
it("should preserve existing query parameters", () => {
|
|
62
|
+
const url = helper.buildLocaleUrl("fr", "/home", { page: "2" });
|
|
63
|
+
expect(url).toBe("/home?page=2");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("setLocaleCookie", () => {
|
|
68
|
+
it("should set cookie with correct name and value", () => {
|
|
69
|
+
const mockRes = {
|
|
70
|
+
setCookie: jest.fn(),
|
|
71
|
+
};
|
|
72
|
+
helper.setLocaleCookie(mockRes, "fr");
|
|
73
|
+
expect(mockRes.setCookie).toHaveBeenCalledWith("locale", "fr", expect.objectContaining({
|
|
74
|
+
path: "/",
|
|
75
|
+
maxAge: 365 * 24 * 60 * 60,
|
|
76
|
+
httpOnly: false,
|
|
77
|
+
sameSite: "lax",
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("validateLocale", () => {
|
|
82
|
+
it("should return valid locale", () => {
|
|
83
|
+
expect(helper.validateLocale("fr")).toBe("fr");
|
|
84
|
+
expect(helper.validateLocale("es")).toBe("es");
|
|
85
|
+
});
|
|
86
|
+
it("should return default locale for invalid locale", () => {
|
|
87
|
+
expect(helper.validateLocale("de")).toBe("en");
|
|
88
|
+
expect(helper.validateLocale("invalid")).toBe("en");
|
|
89
|
+
});
|
|
90
|
+
it("should return default locale for empty string", () => {
|
|
91
|
+
expect(helper.validateLocale("")).toBe("en");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("getClientConfig", () => {
|
|
95
|
+
it("should return client configuration", () => {
|
|
96
|
+
const config = helper.getClientConfig();
|
|
97
|
+
expect(config).toEqual({
|
|
98
|
+
locales: ["en", "fr", "es"],
|
|
99
|
+
defaultLocale: "en",
|
|
100
|
+
urlPattern: "query",
|
|
101
|
+
queryParam: "lang",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const testing_1 = require("@nestjs/testing");
|
|
4
|
+
const i18n_interceptor_1 = require("../i18n.interceptor");
|
|
5
|
+
const i18n_module_options_1 = require("../i18n-module.options");
|
|
6
|
+
const rxjs_1 = require("rxjs");
|
|
7
|
+
describe("I18nInterceptor", () => {
|
|
8
|
+
let interceptor;
|
|
9
|
+
const mockOptions = {
|
|
10
|
+
defaultLocale: "en",
|
|
11
|
+
locales: ["en", "fr", "es"],
|
|
12
|
+
urlPattern: "query",
|
|
13
|
+
translationsPath: "./dictionaries",
|
|
14
|
+
cookieName: "locale",
|
|
15
|
+
queryParam: "lang",
|
|
16
|
+
};
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
const module = await testing_1.Test.createTestingModule({
|
|
19
|
+
providers: [
|
|
20
|
+
i18n_interceptor_1.I18nInterceptor,
|
|
21
|
+
{
|
|
22
|
+
provide: i18n_module_options_1.I18N_MODULE_OPTIONS,
|
|
23
|
+
useValue: mockOptions,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}).compile();
|
|
27
|
+
interceptor = module.get(i18n_interceptor_1.I18nInterceptor);
|
|
28
|
+
});
|
|
29
|
+
const createMockContext = (request, response) => {
|
|
30
|
+
const mockResponse = response || {
|
|
31
|
+
header: jest.fn(),
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
switchToHttp: () => ({
|
|
35
|
+
getRequest: () => request,
|
|
36
|
+
getResponse: () => mockResponse,
|
|
37
|
+
getNext: jest.fn(),
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const createMockCallHandler = () => {
|
|
42
|
+
return {
|
|
43
|
+
handle: () => (0, rxjs_1.of)({}),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
describe("Query pattern locale detection", () => {
|
|
47
|
+
it("should extract locale from query parameter", (done) => {
|
|
48
|
+
const mockRequest = {
|
|
49
|
+
query: { lang: "fr" },
|
|
50
|
+
headers: {},
|
|
51
|
+
};
|
|
52
|
+
const context = createMockContext(mockRequest);
|
|
53
|
+
const next = createMockCallHandler();
|
|
54
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
55
|
+
expect(mockRequest.locale).toBe("fr");
|
|
56
|
+
done();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("should ignore invalid query locale", (done) => {
|
|
60
|
+
const mockRequest = {
|
|
61
|
+
query: { lang: "invalid" },
|
|
62
|
+
headers: {},
|
|
63
|
+
};
|
|
64
|
+
const context = createMockContext(mockRequest);
|
|
65
|
+
const next = createMockCallHandler();
|
|
66
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
67
|
+
expect(mockRequest.locale).toBe("en");
|
|
68
|
+
done();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("Cookie locale detection", () => {
|
|
73
|
+
it("should extract locale from cookie", (done) => {
|
|
74
|
+
const mockRequest = {
|
|
75
|
+
query: {},
|
|
76
|
+
headers: {
|
|
77
|
+
cookie: "locale=fr; other=value",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const context = createMockContext(mockRequest);
|
|
81
|
+
const next = createMockCallHandler();
|
|
82
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
83
|
+
expect(mockRequest.locale).toBe("fr");
|
|
84
|
+
done();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("should ignore invalid cookie locale", (done) => {
|
|
88
|
+
const mockRequest = {
|
|
89
|
+
query: {},
|
|
90
|
+
headers: {
|
|
91
|
+
cookie: "locale=invalid",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const context = createMockContext(mockRequest);
|
|
95
|
+
const next = createMockCallHandler();
|
|
96
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
97
|
+
expect(mockRequest.locale).toBe("en");
|
|
98
|
+
done();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("Header locale detection", () => {
|
|
103
|
+
it("should extract locale from x-lang header when using header pattern", (done) => {
|
|
104
|
+
const headerInterceptor = new i18n_interceptor_1.I18nInterceptor({
|
|
105
|
+
...mockOptions,
|
|
106
|
+
urlPattern: "header",
|
|
107
|
+
});
|
|
108
|
+
const mockRequest = {
|
|
109
|
+
query: {},
|
|
110
|
+
headers: {
|
|
111
|
+
"x-lang": "fr",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const context = createMockContext(mockRequest);
|
|
115
|
+
const next = createMockCallHandler();
|
|
116
|
+
headerInterceptor.intercept(context, next).subscribe(() => {
|
|
117
|
+
expect(mockRequest.locale).toBe("fr");
|
|
118
|
+
done();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it("should extract locale from accept-language header as fallback", (done) => {
|
|
122
|
+
const mockRequest = {
|
|
123
|
+
query: {},
|
|
124
|
+
headers: {
|
|
125
|
+
"accept-language": "fr-FR,fr;q=0.9,en;q=0.8",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const context = createMockContext(mockRequest);
|
|
129
|
+
const next = createMockCallHandler();
|
|
130
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
131
|
+
expect(mockRequest.locale).toBe("fr");
|
|
132
|
+
done();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("Locale priority", () => {
|
|
137
|
+
it("should prioritize query over cookie", (done) => {
|
|
138
|
+
const mockRequest = {
|
|
139
|
+
query: { lang: "es" },
|
|
140
|
+
headers: {
|
|
141
|
+
cookie: "locale=fr",
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const context = createMockContext(mockRequest);
|
|
145
|
+
const next = createMockCallHandler();
|
|
146
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
147
|
+
expect(mockRequest.locale).toBe("es");
|
|
148
|
+
done();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("should prioritize cookie over accept-language header", (done) => {
|
|
152
|
+
const mockRequest = {
|
|
153
|
+
query: {},
|
|
154
|
+
headers: {
|
|
155
|
+
cookie: "locale=fr",
|
|
156
|
+
"accept-language": "es-ES,es;q=0.9,en;q=0.8",
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const context = createMockContext(mockRequest);
|
|
160
|
+
const next = createMockCallHandler();
|
|
161
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
162
|
+
expect(mockRequest.locale).toBe("fr");
|
|
163
|
+
done();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it("should fall back to default locale when nothing matches", (done) => {
|
|
167
|
+
const mockRequest = {
|
|
168
|
+
query: {},
|
|
169
|
+
headers: {},
|
|
170
|
+
};
|
|
171
|
+
const context = createMockContext(mockRequest);
|
|
172
|
+
const next = createMockCallHandler();
|
|
173
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
174
|
+
expect(mockRequest.locale).toBe("en");
|
|
175
|
+
done();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
it("should set cookie when locale is found in URL", (done) => {
|
|
179
|
+
const mockResponse = {
|
|
180
|
+
header: jest.fn(),
|
|
181
|
+
};
|
|
182
|
+
const mockRequest = {
|
|
183
|
+
query: { lang: "fr" },
|
|
184
|
+
headers: {},
|
|
185
|
+
};
|
|
186
|
+
const context = createMockContext(mockRequest, mockResponse);
|
|
187
|
+
const next = createMockCallHandler();
|
|
188
|
+
interceptor.intercept(context, next).subscribe(() => {
|
|
189
|
+
expect(mockRequest.locale).toBe("fr");
|
|
190
|
+
expect(mockResponse.header).toHaveBeenCalledWith("Set-Cookie", expect.stringContaining("locale=fr"));
|
|
191
|
+
done();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const testing_1 = require("@nestjs/testing");
|
|
4
|
+
const i18n_module_1 = require("../i18n.module");
|
|
5
|
+
const i18n_service_1 = require("../i18n.service");
|
|
6
|
+
const i18n_helper_1 = require("../i18n.helper");
|
|
7
|
+
const i18n_switcher_controller_1 = require("../i18n-switcher.controller");
|
|
8
|
+
const i18n_module_options_1 = require("../i18n-module.options");
|
|
9
|
+
describe("I18nModule", () => {
|
|
10
|
+
it("should be defined", () => {
|
|
11
|
+
expect(i18n_module_1.I18nModule).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
describe("forRoot", () => {
|
|
14
|
+
it("should create a dynamic module with default options", async () => {
|
|
15
|
+
const module = i18n_module_1.I18nModule.forRoot({
|
|
16
|
+
defaultLocale: "en",
|
|
17
|
+
locales: ["en", "fr"],
|
|
18
|
+
});
|
|
19
|
+
expect(module.module).toBe(i18n_module_1.I18nModule);
|
|
20
|
+
expect(module.controllers).toContain(i18n_switcher_controller_1.I18nSwitcherController);
|
|
21
|
+
expect(module.providers).toBeDefined();
|
|
22
|
+
expect(module.exports).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
it("should provide I18nService", async () => {
|
|
25
|
+
const testModule = await testing_1.Test.createTestingModule({
|
|
26
|
+
imports: [
|
|
27
|
+
i18n_module_1.I18nModule.forRoot({
|
|
28
|
+
defaultLocale: "en",
|
|
29
|
+
locales: ["en", "fr"],
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
}).compile();
|
|
33
|
+
const service = await testModule.resolve(i18n_service_1.I18nService);
|
|
34
|
+
expect(service).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
it("should provide I18nHelper", async () => {
|
|
37
|
+
const testModule = await testing_1.Test.createTestingModule({
|
|
38
|
+
imports: [
|
|
39
|
+
i18n_module_1.I18nModule.forRoot({
|
|
40
|
+
defaultLocale: "en",
|
|
41
|
+
locales: ["en", "fr"],
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
}).compile();
|
|
45
|
+
const helper = testModule.get(i18n_helper_1.I18nHelper);
|
|
46
|
+
expect(helper).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
it("should apply default values for missing options", async () => {
|
|
49
|
+
const module = i18n_module_1.I18nModule.forRoot({
|
|
50
|
+
defaultLocale: "en",
|
|
51
|
+
locales: ["en"],
|
|
52
|
+
});
|
|
53
|
+
const optionsProvider = module.providers?.find((p) => p.provide === i18n_module_options_1.I18N_MODULE_OPTIONS);
|
|
54
|
+
expect(optionsProvider.useValue).toEqual({
|
|
55
|
+
defaultLocale: "en",
|
|
56
|
+
locales: ["en"],
|
|
57
|
+
urlPattern: "query",
|
|
58
|
+
translationsPath: "./dictionaries",
|
|
59
|
+
cookieName: "locale",
|
|
60
|
+
queryParam: "lang",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it("should preserve custom options", async () => {
|
|
64
|
+
const module = i18n_module_1.I18nModule.forRoot({
|
|
65
|
+
defaultLocale: "fr",
|
|
66
|
+
locales: ["fr", "en", "es"],
|
|
67
|
+
urlPattern: "header",
|
|
68
|
+
translationsPath: "../locales",
|
|
69
|
+
cookieName: "user_locale",
|
|
70
|
+
queryParam: "language",
|
|
71
|
+
});
|
|
72
|
+
const optionsProvider = module.providers?.find((p) => p.provide === i18n_module_options_1.I18N_MODULE_OPTIONS);
|
|
73
|
+
expect(optionsProvider.useValue).toEqual({
|
|
74
|
+
defaultLocale: "fr",
|
|
75
|
+
locales: ["fr", "en", "es"],
|
|
76
|
+
urlPattern: "header",
|
|
77
|
+
translationsPath: "../locales",
|
|
78
|
+
cookieName: "user_locale",
|
|
79
|
+
queryParam: "language",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|