@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
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,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,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 {};
|