@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.
Files changed (147) hide show
  1. package/README.md +326 -0
  2. package/dist/cli.d.ts +12 -0
  3. package/dist/cli.js +53 -0
  4. package/dist/client/Link.d.ts +5 -0
  5. package/dist/client/Link.js +62 -0
  6. package/dist/client/__tests__/getActiveItemId.test.d.ts +1 -0
  7. package/dist/client/__tests__/getActiveItemId.test.js +38 -0
  8. package/dist/client/getActiveItemId.d.ts +7 -0
  9. package/dist/client/getActiveItemId.js +55 -0
  10. package/dist/client/use-i18n.d.ts +7 -0
  11. package/dist/client/use-i18n.js +64 -0
  12. package/dist/core/__tests__/component-analyzer.test.d.ts +1 -0
  13. package/dist/core/__tests__/component-analyzer.test.js +151 -0
  14. package/dist/core/__tests__/hydration-manifest.test.d.ts +1 -0
  15. package/dist/core/__tests__/hydration-manifest.test.js +211 -0
  16. package/dist/core/__tests__/jsx.engine.test.d.ts +1 -0
  17. package/dist/core/__tests__/jsx.engine.test.js +118 -0
  18. package/dist/core/app-setup.d.ts +7 -0
  19. package/dist/core/app-setup.js +79 -0
  20. package/dist/core/auto-register.module.d.ts +9 -0
  21. package/dist/core/auto-register.module.js +18 -0
  22. package/dist/core/auto-wrap-middleware.d.ts +4 -0
  23. package/dist/core/auto-wrap-middleware.js +130 -0
  24. package/dist/core/client-component-wrapper.d.ts +5 -0
  25. package/dist/core/client-component-wrapper.js +37 -0
  26. package/dist/core/client-hydration.d.ts +2 -0
  27. package/dist/core/client-hydration.js +93 -0
  28. package/dist/core/client-wrapper-browser.d.ts +2 -0
  29. package/dist/core/client-wrapper-browser.js +22 -0
  30. package/dist/core/component-analyzer.d.ts +4 -0
  31. package/dist/core/component-analyzer.js +98 -0
  32. package/dist/core/component-auto-wrapper.d.ts +2 -0
  33. package/dist/core/component-auto-wrapper.js +63 -0
  34. package/dist/core/component-client-wrapper.d.ts +4 -0
  35. package/dist/core/component-client-wrapper.js +80 -0
  36. package/dist/core/hydration-generator.d.ts +2 -0
  37. package/dist/core/hydration-generator.js +98 -0
  38. package/dist/core/hydration-manifest.d.ts +7 -0
  39. package/dist/core/hydration-manifest.js +83 -0
  40. package/dist/core/hydration.d.ts +16 -0
  41. package/dist/core/hydration.js +72 -0
  42. package/dist/core/jsx.engine.d.ts +9 -0
  43. package/dist/core/jsx.engine.js +161 -0
  44. package/dist/core/live-reload-client.js +32 -0
  45. package/dist/core/live-reload.controller.d.ts +10 -0
  46. package/dist/core/live-reload.controller.js +38 -0
  47. package/dist/core/navigation.service.d.ts +18 -0
  48. package/dist/core/navigation.service.js +206 -0
  49. package/dist/core/router.module.d.ts +2 -0
  50. package/dist/core/router.module.js +21 -0
  51. package/dist/core/static-assets.controller.d.ts +4 -0
  52. package/dist/core/static-assets.controller.js +51 -0
  53. package/dist/core/types/nav.types.d.ts +22 -0
  54. package/dist/core/types/nav.types.js +2 -0
  55. package/dist/core/views/layout.d.ts +8 -0
  56. package/dist/core/views/layout.js +35 -0
  57. package/dist/decorators/jsx.decorator.d.ts +26 -0
  58. package/dist/decorators/jsx.decorator.js +10 -0
  59. package/dist/decorators/layout.decorator.d.ts +4 -0
  60. package/dist/decorators/layout.decorator.js +29 -0
  61. package/dist/i18n/__tests__/i18n.helper.test.d.ts +1 -0
  62. package/dist/i18n/__tests__/i18n.helper.test.js +105 -0
  63. package/dist/i18n/__tests__/i18n.interceptor.test.d.ts +1 -0
  64. package/dist/i18n/__tests__/i18n.interceptor.test.js +195 -0
  65. package/dist/i18n/__tests__/i18n.module.test.d.ts +1 -0
  66. package/dist/i18n/__tests__/i18n.module.test.js +83 -0
  67. package/dist/i18n/__tests__/i18n.service.test.d.ts +1 -0
  68. package/dist/i18n/__tests__/i18n.service.test.js +109 -0
  69. package/dist/i18n/__tests__/t.test.d.ts +1 -0
  70. package/dist/i18n/__tests__/t.test.js +66 -0
  71. package/dist/i18n/i18n-module.options.d.ts +10 -0
  72. package/dist/i18n/i18n-module.options.js +4 -0
  73. package/dist/i18n/i18n-switcher.controller.d.ts +12 -0
  74. package/dist/i18n/i18n-switcher.controller.js +80 -0
  75. package/dist/i18n/i18n-types.d.ts +8 -0
  76. package/dist/i18n/i18n-types.js +2 -0
  77. package/dist/i18n/i18n.helper.d.ts +14 -0
  78. package/dist/i18n/i18n.helper.js +70 -0
  79. package/dist/i18n/i18n.interceptor.d.ts +9 -0
  80. package/dist/i18n/i18n.interceptor.js +99 -0
  81. package/dist/i18n/i18n.module.d.ts +5 -0
  82. package/dist/i18n/i18n.module.js +51 -0
  83. package/dist/i18n/i18n.service.d.ts +12 -0
  84. package/dist/i18n/i18n.service.js +61 -0
  85. package/dist/i18n/index.d.ts +10 -0
  86. package/dist/i18n/index.js +20 -0
  87. package/dist/i18n/locale.decorator.d.ts +1 -0
  88. package/dist/i18n/locale.decorator.js +8 -0
  89. package/dist/i18n/t.d.ts +3 -0
  90. package/dist/i18n/t.js +16 -0
  91. package/dist/index.d.ts +19 -0
  92. package/dist/index.js +40 -0
  93. package/package.json +79 -0
  94. package/scripts/analyze-styles.ts +124 -0
  95. package/scripts/auto-wrap-exports.ts +239 -0
  96. package/scripts/build-css.ts +38 -0
  97. package/scripts/build-hydration.ts +313 -0
  98. package/scripts/build-page-styles.ts +43 -0
  99. package/scripts/copy-assets.ts +34 -0
  100. package/scripts/dev.sh +3 -0
  101. package/scripts/dev.ts +257 -0
  102. package/src/cli.ts +71 -0
  103. package/src/client/Link.tsx +62 -0
  104. package/src/client/__tests__/getActiveItemId.test.ts +49 -0
  105. package/src/client/getActiveItemId.ts +54 -0
  106. package/src/client/use-i18n.ts +111 -0
  107. package/src/core/__tests__/component-analyzer.test.ts +141 -0
  108. package/src/core/__tests__/hydration-manifest.test.ts +223 -0
  109. package/src/core/__tests__/jsx.engine.test.ts +137 -0
  110. package/src/core/app-setup.ts +114 -0
  111. package/src/core/auto-register.module.ts +30 -0
  112. package/src/core/auto-wrap-middleware.ts +165 -0
  113. package/src/core/client-component-wrapper.ts +72 -0
  114. package/src/core/client-hydration.tsx +99 -0
  115. package/src/core/client-wrapper-browser.ts +40 -0
  116. package/src/core/component-analyzer.ts +89 -0
  117. package/src/core/component-auto-wrapper.ts +68 -0
  118. package/src/core/component-client-wrapper.ts +112 -0
  119. package/src/core/hydration-generator.ts +94 -0
  120. package/src/core/hydration-manifest.ts +79 -0
  121. package/src/core/hydration.ts +70 -0
  122. package/src/core/jsx.engine.ts +205 -0
  123. package/src/core/live-reload-client.js +32 -0
  124. package/src/core/live-reload.controller.ts +55 -0
  125. package/src/core/navigation.service.ts +257 -0
  126. package/src/core/router.module.ts +9 -0
  127. package/src/core/static-assets.controller.ts +19 -0
  128. package/src/core/types/nav.types.ts +53 -0
  129. package/src/core/views/layout.tsx +61 -0
  130. package/src/decorators/jsx.decorator.ts +49 -0
  131. package/src/decorators/layout.decorator.ts +66 -0
  132. package/src/i18n/__tests__/i18n.helper.test.ts +126 -0
  133. package/src/i18n/__tests__/i18n.interceptor.test.ts +229 -0
  134. package/src/i18n/__tests__/i18n.module.test.ts +98 -0
  135. package/src/i18n/__tests__/i18n.service.test.ts +129 -0
  136. package/src/i18n/__tests__/t.test.ts +88 -0
  137. package/src/i18n/i18n-module.options.ts +53 -0
  138. package/src/i18n/i18n-switcher.controller.ts +99 -0
  139. package/src/i18n/i18n-types.ts +56 -0
  140. package/src/i18n/i18n.helper.ts +75 -0
  141. package/src/i18n/i18n.interceptor.ts +114 -0
  142. package/src/i18n/i18n.module.ts +45 -0
  143. package/src/i18n/i18n.service.ts +95 -0
  144. package/src/i18n/index.ts +37 -0
  145. package/src/i18n/locale.decorator.ts +10 -0
  146. package/src/i18n/t.ts +62 -0
  147. package/src/index.ts +31 -0
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # @harpy-js/core
2
+
3
+ Core package for NestJS + React/JSX with server-side rendering and automatic client-side hydration.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **JSX Engine** - Render React components in NestJS controllers
8
+ - 🔄 **Auto Hydration** - Client components marked with `'use client'` automatically hydrate
9
+ - ⚡ **Fast Builds** - Optimized build pipeline with esbuild
10
+ - 🚀 **Performance Optimized** - Shared vendor bundle (188KB) + tiny component chunks (1-3KB)
11
+ - 📦 **Zero Config** - Works out of the box with NestJS
12
+ - 🌐 **I18n Support** - Built-in internationalization with type-safe translations
13
+ - 🍪 **Cookie Management** - Integrated with Fastify for session management
14
+ - 🎨 **CSS Optimization** - Automatic minification with cssnano in production
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @harpy-js/core react react-dom
20
+ # or
21
+ yarn add @harpy-js/core react react-dom
22
+ # or
23
+ pnpm add @harpy-js/core react react-dom
24
+ ```
25
+
26
+ **Required peer dependencies:**
27
+
28
+ - `@nestjs/common` ^11.0.0
29
+ - `@nestjs/core` ^11.0.0
30
+ - `@nestjs/platform-fastify` ^11.0.0
31
+ - `react` ^19.0.0
32
+ - `react-dom` ^19.0.0
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Set up the JSX engine in your main.ts
37
+
38
+ ```typescript
39
+ import "reflect-metadata"; // Required for NestJS
40
+ import { NestFactory } from "@nestjs/core";
41
+ import {
42
+ FastifyAdapter,
43
+ NestFastifyApplication,
44
+ } from "@nestjs/platform-fastify";
45
+ import { withJsxEngine } from "@harpy-js/core";
46
+ import { AppModule } from "./app.module";
47
+ import DefaultLayout from "./views/layout";
48
+ import * as path from "path";
49
+ import fastifyStatic from "@fastify/static";
50
+
51
+ async function bootstrap() {
52
+ const app = await NestFactory.create<NestFastifyApplication>(
53
+ AppModule,
54
+ new FastifyAdapter(),
55
+ );
56
+
57
+ // Enable JSX rendering
58
+ withJsxEngine(app, DefaultLayout);
59
+
60
+ // Register static file serving
61
+ const fastify = app.getHttpAdapter().getInstance();
62
+ await fastify.register(fastifyStatic, {
63
+ root: path.join(process.cwd(), "dist"),
64
+ prefix: "/",
65
+ });
66
+
67
+ await app.listen({
68
+ port: 3000,
69
+ host: "0.0.0.0",
70
+ });
71
+ }
72
+
73
+ bootstrap();
74
+ ```
75
+
76
+ ### 2. Create a layout component
77
+
78
+ ```tsx
79
+ // src/views/layout.tsx
80
+ import React from "react";
81
+ import { JsxLayoutProps } from "@ harpy-js/core";
82
+
83
+ export default function Layout({
84
+ children,
85
+ meta,
86
+ hydrationScripts,
87
+ }: JsxLayoutProps) {
88
+ return (
89
+ <html lang="en">
90
+ <head>
91
+ <meta charSet="utf-8" />
92
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
93
+ <title>{meta?.title || "My App"}</title>
94
+ {meta?.description && (
95
+ <meta name="description" content={meta.description} />
96
+ )}
97
+ <link rel="stylesheet" href="/styles/styles.css" />
98
+ </head>
99
+ <body>
100
+ <main id="body">{children}</main>
101
+ {/* Vendor bundle loads React/ReactDOM once */}
102
+ {hydrationScripts?.vendorScript && (
103
+ <script src={hydrationScripts.vendorScript} />
104
+ )}
105
+ {/* Component-specific hydration scripts */}
106
+ {hydrationScripts?.componentScripts?.map((script) => (
107
+ <script key={script.componentName} src={script.path} />
108
+ ))}
109
+ </body>
110
+ </html>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ### 3. Create a controller with JSX rendering
116
+
117
+ ```typescript
118
+ import { Controller, Get } from "@nestjs/common";
119
+ import { JsxRender } from "@ harpy-js/core";
120
+ import Homepage from "./views/homepage";
121
+
122
+ @Controller()
123
+ export class HomeController {
124
+ @Get()
125
+ @JsxRender(Homepage, {
126
+ meta: {
127
+ title: "Welcome",
128
+ description: "My homepage",
129
+ },
130
+ })
131
+ home() {
132
+ return {
133
+ message: "Hello World",
134
+ };
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### 4. Create your page component
140
+
141
+ ```tsx
142
+ // src/features/home/views/homepage.tsx
143
+ import React from "react";
144
+ import Counter from "./counter";
145
+
146
+ export default function Homepage({ message }) {
147
+ return (
148
+ <div>
149
+ <h1>{message}</h1>
150
+ <Counter />
151
+ </div>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ### 5. Create a client component
157
+
158
+ ```tsx
159
+ // src/features/home/views/counter.tsx
160
+ "use client";
161
+
162
+ import React from "react";
163
+
164
+ export default function Counter() {
165
+ const [count, setCount] = React.useState(0);
166
+
167
+ return (
168
+ <div>
169
+ <p>Count: {count}</p>
170
+ <button onClick={() => setCount(count + 1)}>Increment</button>
171
+ </div>
172
+ );
173
+ }
174
+ ```
175
+
176
+ ## API Reference
177
+
178
+ ### `withJsxEngine(app, defaultLayout)`
179
+
180
+ Sets up the JSX rendering engine on a NestJS Fastify application.
181
+
182
+ **Parameters:**
183
+
184
+ - `app` - NestFastifyApplication instance
185
+ - `defaultLayout` - React component to use as the default layout
186
+
187
+ ### `@JsxRender(component, options?)`
188
+
189
+ Decorator to render a React component from a controller method.
190
+
191
+ **Parameters:**
192
+
193
+ - `component` - React component to render
194
+ - `options` - Rendering options
195
+ - `meta` - Meta tags for the page (title, description, og tags, etc.)
196
+ - `layout` - Custom layout component (optional)
197
+
198
+ ### `autoWrapClientComponent(Component, componentName)`
199
+
200
+ Wraps a component for automatic hydration. Used internally by the build process.
201
+
202
+ ## Build Scripts
203
+
204
+ The package includes CLI commands for building:
205
+
206
+ ```bash
207
+ # Build hydration bundles
208
+ harpy build-hydration
209
+
210
+ # Auto-wrap client components
211
+ harpy auto-wrap
212
+
213
+ # Build styles
214
+ harpy build-styles
215
+
216
+ # Development mode
217
+ harpy dev
218
+ ```
219
+
220
+ ## How It Works
221
+
222
+ 1. **Server-Side Rendering**: Components are rendered to HTML on the server
223
+ 2. **Component Registration**: Client components (marked with `'use client'`) register themselves during SSR
224
+ 3. **Auto-Wrapping**: Build scripts automatically wrap client components for hydration
225
+ 4. **Vendor Bundle Optimization**: React and ReactDOM are bundled once (188KB) and shared across all components
226
+ 5. **Client Bundling**: Client components are bundled separately with esbuild (1-3KB each)
227
+ 6. **Hydration**: Client bundles load React from the shared vendor and hydrate the SSR'd HTML
228
+
229
+ ## Performance Optimizations
230
+
231
+ The framework implements several performance optimizations:
232
+
233
+ - **Shared Vendor Bundle**: React (19.x) and ReactDOM are bundled once (188KB minified) instead of being duplicated in each component
234
+ - **Tiny Component Chunks**: Individual components are only 1-3KB each (97% reduction compared to bundling React in each)
235
+ - **Tree Shaking**: Unused code is automatically removed during production builds
236
+ - **CSS Minification**: Stylesheets are automatically minified with cssnano in production
237
+ - **Production Mode**: `process.env.NODE_ENV` is properly set to enable React optimizations
238
+
239
+ ## Internationalization (i18n)
240
+
241
+ Built-in support for multi-language applications:
242
+
243
+ ```typescript
244
+ // app.module.ts
245
+ import { Module } from "@nestjs/common";
246
+ import { I18nModule } from "@ harpy-js/i18n";
247
+
248
+ @Module({
249
+ imports: [
250
+ I18nModule.forRoot({
251
+ defaultLocale: "en",
252
+ supportedLocales: ["en", "fr", "ar"],
253
+ dictionaries: {
254
+ en: () => import("./dictionaries/en.json"),
255
+ fr: () => import("./dictionaries/fr.json"),
256
+ ar: () => import("./dictionaries/ar.json"),
257
+ },
258
+ }),
259
+ ],
260
+ })
261
+ export class AppModule {}
262
+ ```
263
+
264
+ **Using translations in components:**
265
+
266
+ ```tsx
267
+ // Client component
268
+ "use client";
269
+ import { useI18n } from "@ harpy-js/core/client";
270
+
271
+ export default function MyComponent() {
272
+ const { t, locale, setLocale } = useI18n();
273
+
274
+ return (
275
+ <div>
276
+ <h1>{t("welcome.title")}</h1>
277
+ <button onClick={() => setLocale("fr")}>Switch to French</button>
278
+ </div>
279
+ );
280
+ }
281
+ ```
282
+
283
+ **Using translations in controllers (server-side):**
284
+
285
+ ```typescript
286
+ import { Controller, Get } from "@nestjs/common";
287
+ import { JsxRender } from "@ harpy-js/core";
288
+ import { CurrentLocale, t } from "@ harpy-js/i18n";
289
+ import { getDictionary } from "./i18n/get-dictionary";
290
+
291
+ @Controller()
292
+ export class HomeController {
293
+ @Get()
294
+ @JsxRender(Homepage)
295
+ async home(@CurrentLocale() locale: string) {
296
+ const dict = await getDictionary(locale);
297
+
298
+ return {
299
+ title: t(dict, "home.title"),
300
+ message: t(dict, "home.welcome"),
301
+ dict,
302
+ locale,
303
+ };
304
+ }
305
+ }
306
+ ```
307
+
308
+ ## TypeScript Configuration
309
+
310
+ Make sure your `tsconfig.json` includes:
311
+
312
+ ```json
313
+ {
314
+ "compilerOptions": {
315
+ "jsx": "react",
316
+ "esModuleInterop": true,
317
+ "paths": {
318
+ "@/*": ["src/*"]
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
324
+ ## License
325
+
326
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ declare const spawn: any;
3
+ declare const path: any;
4
+ declare const command: string;
5
+ declare const args: string[];
6
+ declare const scripts: Record<string, string>;
7
+ declare const scriptPath: string;
8
+ declare const findTsx: () => string;
9
+ declare const tsxCmd: string;
10
+ declare let execCommand: string;
11
+ declare let cmdArgs: string[];
12
+ declare const proc: any;
package/dist/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const { spawn } = require("child_process");
4
+ const path = require("path");
5
+ const command = process.argv[2];
6
+ const args = process.argv.slice(3);
7
+ const scripts = {
8
+ "build-hydration": path.join(__dirname, "../scripts/build-hydration.ts"),
9
+ "auto-wrap": path.join(__dirname, "../scripts/auto-wrap-exports.ts"),
10
+ "build-styles": path.join(__dirname, "../scripts/build-page-styles.ts"),
11
+ dev: path.join(__dirname, "../scripts/dev.ts"),
12
+ };
13
+ if (!command || !scripts[command]) {
14
+ console.error("Usage: harpy <command>");
15
+ console.error("Commands: build-hydration, auto-wrap, build-styles, dev");
16
+ process.exit(1);
17
+ }
18
+ const scriptPath = scripts[command];
19
+ const findTsx = () => {
20
+ const fs = require("fs");
21
+ const possiblePaths = [
22
+ path.join(process.cwd(), "node_modules", ".bin", "tsx"),
23
+ path.join(process.cwd(), "apps", "test-app", "node_modules", ".bin", "tsx"),
24
+ path.join(__dirname, "../../node_modules", ".bin", "tsx"),
25
+ path.join(__dirname, "../../../node_modules", ".bin", "tsx"),
26
+ path.join(__dirname, "../../../../node_modules", ".bin", "tsx"),
27
+ ];
28
+ for (const tsxPath of possiblePaths) {
29
+ if (fs.existsSync(tsxPath)) {
30
+ return tsxPath;
31
+ }
32
+ }
33
+ throw new Error("tsx not found. Please install tsx in your project: npm install -D tsx");
34
+ };
35
+ const tsxCmd = findTsx();
36
+ let execCommand;
37
+ let cmdArgs;
38
+ if (tsxCmd.startsWith("node ")) {
39
+ const parts = tsxCmd.split(" ");
40
+ execCommand = parts[0];
41
+ cmdArgs = [...parts.slice(1), scriptPath, ...args];
42
+ }
43
+ else {
44
+ execCommand = tsxCmd;
45
+ cmdArgs = [scriptPath, ...args];
46
+ }
47
+ const proc = spawn(execCommand, cmdArgs, {
48
+ stdio: "inherit",
49
+ shell: false,
50
+ });
51
+ proc.on("exit", (code) => {
52
+ process.exit(code || 0);
53
+ });
@@ -0,0 +1,5 @@
1
+ import React, { AnchorHTMLAttributes } from "react";
2
+ export type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
3
+ replace?: boolean;
4
+ };
5
+ export default function Link({ href, onClick, replace, ...rest }: LinkProps): React.JSX.Element;
@@ -0,0 +1,62 @@
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.default = Link;
37
+ const react_1 = __importStar(require("react"));
38
+ function Link({ href = "#", onClick, replace, ...rest }) {
39
+ const isLocal = typeof href === "string" && href.startsWith("/");
40
+ const handleClick = (0, react_1.useCallback)((e) => {
41
+ if (onClick) {
42
+ onClick(e);
43
+ }
44
+ if (e.defaultPrevented)
45
+ return;
46
+ if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
47
+ return;
48
+ }
49
+ if (!isLocal)
50
+ return;
51
+ e.preventDefault();
52
+ try {
53
+ const method = replace ? "replaceState" : "pushState";
54
+ window.history[method]({}, "", href);
55
+ window.dispatchEvent(new PopStateEvent("popstate"));
56
+ }
57
+ catch (err) {
58
+ window.location.assign(href);
59
+ }
60
+ }, [href, isLocal, onClick, replace]);
61
+ return react_1.default.createElement("a", { href: href, onClick: handleClick, ...rest });
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const getActiveItemId_1 = require("../getActiveItemId");
4
+ describe('getActiveItemId helpers', () => {
5
+ const items = [
6
+ { id: 'home', href: '/' },
7
+ { id: 'docs', href: '/docs' },
8
+ { id: 'docs-start', href: '/docs/getting-started' },
9
+ { id: 'about', href: '/about/' },
10
+ ];
11
+ test('exact match prefers exact href over ancestor', () => {
12
+ const id = (0, getActiveItemId_1.getActiveItemIdFromManifest)(items, '/docs/getting-started');
13
+ expect(id).toBe('docs-start');
14
+ });
15
+ test('ancestor matching finds parent when exact missing', () => {
16
+ const id = (0, getActiveItemId_1.getActiveItemIdFromManifest)(items, '/docs/usage');
17
+ expect(id).toBe('docs');
18
+ });
19
+ test('normalizes query and fragment and trailing slash', () => {
20
+ const id = (0, getActiveItemId_1.getActiveItemIdFromManifest)(items, '/about?lang=en#team');
21
+ expect(id).toBe('about');
22
+ });
23
+ test('root fallback matches when no other match', () => {
24
+ const id = (0, getActiveItemId_1.getActiveItemIdFromManifest)(items, '/unknown/path');
25
+ expect(id).toBe('home');
26
+ });
27
+ test('returns undefined when no match and no root', () => {
28
+ const itemsNoRoot = items.filter((i) => i.href !== '/');
29
+ const id = (0, getActiveItemId_1.getActiveItemIdFromManifest)(itemsNoRoot, '/unknown/path');
30
+ expect(id).toBeUndefined();
31
+ });
32
+ test('index-based lookup returns same result as manifest-based', () => {
33
+ const idx = (0, getActiveItemId_1.buildHrefIndex)(items);
34
+ const byIndex = (0, getActiveItemId_1.getActiveItemIdFromIndex)(idx, '/docs/guide/intro');
35
+ const byManifest = (0, getActiveItemId_1.getActiveItemIdFromManifest)(items, '/docs/guide/intro');
36
+ expect(byIndex).toBe(byManifest);
37
+ });
38
+ });
@@ -0,0 +1,7 @@
1
+ export type NavItemLite = {
2
+ id: string;
3
+ href?: string;
4
+ };
5
+ export declare function buildHrefIndex(items: NavItemLite[]): Map<string, string[]>;
6
+ export declare function getActiveItemIdFromIndex(hrefIndex: Map<string, string[]>, currentPath?: string): string | undefined;
7
+ export declare function getActiveItemIdFromManifest(items: NavItemLite[], currentPath?: string): string | undefined;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHrefIndex = buildHrefIndex;
4
+ exports.getActiveItemIdFromIndex = getActiveItemIdFromIndex;
5
+ exports.getActiveItemIdFromManifest = getActiveItemIdFromManifest;
6
+ function normalizePath(p) {
7
+ if (!p)
8
+ return "";
9
+ const withoutQuery = p.split(/[?#]/)[0];
10
+ if (withoutQuery.length > 1 && withoutQuery.endsWith("/"))
11
+ return withoutQuery.slice(0, -1);
12
+ return withoutQuery;
13
+ }
14
+ function buildHrefIndex(items) {
15
+ const map = new Map();
16
+ for (const it of items) {
17
+ if (!it.href)
18
+ continue;
19
+ const key = normalizePath(it.href);
20
+ const arr = map.get(key) ?? [];
21
+ arr.push(it.id);
22
+ map.set(key, arr);
23
+ }
24
+ return map;
25
+ }
26
+ function getActiveItemIdFromIndex(hrefIndex, currentPath) {
27
+ if (!currentPath)
28
+ return undefined;
29
+ let cur = normalizePath(currentPath);
30
+ while (cur !== "") {
31
+ const entry = hrefIndex.get(cur);
32
+ if (entry && entry.length > 0)
33
+ return entry[0];
34
+ const lastSlash = cur.lastIndexOf("/");
35
+ if (lastSlash === -1)
36
+ break;
37
+ if (lastSlash === 0) {
38
+ cur = "/";
39
+ }
40
+ else {
41
+ cur = cur.slice(0, lastSlash);
42
+ }
43
+ if (cur === "/") {
44
+ const entryRoot = hrefIndex.get("/");
45
+ if (entryRoot && entryRoot.length > 0)
46
+ return entryRoot[0];
47
+ break;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+ function getActiveItemIdFromManifest(items, currentPath) {
53
+ const idx = buildHrefIndex(items);
54
+ return getActiveItemIdFromIndex(idx, currentPath);
55
+ }
@@ -0,0 +1,7 @@
1
+ interface UseI18nReturn {
2
+ switchLocale: (locale: string) => void;
3
+ getCurrentLocale: () => string | null;
4
+ buildUrl: (path: string, locale?: string) => string;
5
+ }
6
+ export declare function useI18n(): UseI18nReturn;
7
+ export {};
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.useI18n = useI18n;
5
+ const react_1 = require("react");
6
+ function useI18n() {
7
+ (0, react_1.useEffect)(() => {
8
+ if (typeof window !== "undefined" &&
9
+ !window.__HARPY_I18N_INITIALIZED__) {
10
+ window.__HARPY_I18N_INITIALIZED__ = true;
11
+ const script = document.createElement("script");
12
+ script.textContent = `
13
+ (function() {
14
+ if (window.__HARPY_I18N_NAV_INSTALLED__) return;
15
+ window.__HARPY_I18N_NAV_INSTALLED__ = true;
16
+
17
+ document.addEventListener('click', function(e) {
18
+ var target = e.target;
19
+ while (target && target.tagName !== 'A') {
20
+ target = target.parentElement;
21
+ }
22
+ if (target && target.tagName === 'A' && target.href) {
23
+ var url = new URL(target.href, window.location.origin);
24
+ var currentLang = new URLSearchParams(window.location.search).get('lang');
25
+ if (currentLang && url.origin === window.location.origin && !url.searchParams.has('lang')) {
26
+ url.searchParams.set('lang', currentLang);
27
+ target.href = url.toString();
28
+ }
29
+ }
30
+ });
31
+ })();
32
+ `;
33
+ document.head.appendChild(script);
34
+ }
35
+ }, []);
36
+ const switchLocale = (locale) => {
37
+ if (typeof window === "undefined")
38
+ return;
39
+ const url = new URL(window.location.href);
40
+ url.searchParams.set("lang", locale);
41
+ window.location.href = url.toString();
42
+ };
43
+ const getCurrentLocale = () => {
44
+ if (typeof window === "undefined")
45
+ return null;
46
+ const url = new URL(window.location.href);
47
+ return url.searchParams.get("lang");
48
+ };
49
+ const buildUrl = (path, locale) => {
50
+ if (typeof window === "undefined")
51
+ return path;
52
+ const currentLocale = locale || getCurrentLocale();
53
+ if (!currentLocale)
54
+ return path;
55
+ const url = new URL(path, window.location.origin);
56
+ url.searchParams.set("lang", currentLocale);
57
+ return url.pathname + url.search;
58
+ };
59
+ return {
60
+ switchLocale,
61
+ getCurrentLocale,
62
+ buildUrl,
63
+ };
64
+ }
@@ -0,0 +1 @@
1
+ export {};