frontend-hamroun 1.1.47 → 1.1.48
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
CHANGED
@@ -95,12 +95,222 @@ function ThemedButton() {
|
|
95
95
|
}
|
96
96
|
```
|
97
97
|
|
98
|
-
|
98
|
+
## Server-Side Rendering
|
99
99
|
|
100
|
+
### Quick Start
|
100
101
|
```tsx
|
102
|
+
// server.ts
|
101
103
|
import { renderToString } from 'frontend-hamroun';
|
102
104
|
|
103
|
-
const html = await renderToString(<App
|
105
|
+
const html = await renderToString(<App />, {
|
106
|
+
title: 'My SSR App',
|
107
|
+
description: 'A server-rendered application',
|
108
|
+
scripts: ['/client.js'],
|
109
|
+
initialState: {
|
110
|
+
user: { id: 1, name: 'John' }
|
111
|
+
}
|
112
|
+
});
|
113
|
+
```
|
114
|
+
|
115
|
+
### Complete SSR Setup
|
116
|
+
|
117
|
+
1. Server Setup:
|
118
|
+
```typescript
|
119
|
+
// server.ts
|
120
|
+
import express from 'express';
|
121
|
+
import { renderToString } from 'frontend-hamroun';
|
122
|
+
|
123
|
+
const app = express();
|
124
|
+
|
125
|
+
app.get('*', async (req, res) => {
|
126
|
+
try {
|
127
|
+
const html = await renderToString(<App />, {
|
128
|
+
title: 'My App',
|
129
|
+
description: 'Server-rendered app',
|
130
|
+
scripts: ['/client.js'],
|
131
|
+
styles: ['/styles.css'],
|
132
|
+
meta: {
|
133
|
+
'og:title': 'My App',
|
134
|
+
'theme-color': '#ffffff'
|
135
|
+
},
|
136
|
+
initialState: {
|
137
|
+
url: req.url,
|
138
|
+
user: req.user
|
139
|
+
},
|
140
|
+
bodyAttrs: { class: 'app-root' },
|
141
|
+
htmlAttrs: { lang: 'en' }
|
142
|
+
});
|
143
|
+
|
144
|
+
res.send(html);
|
145
|
+
} catch (error) {
|
146
|
+
res.status(500).send('Server Error');
|
147
|
+
}
|
148
|
+
});
|
149
|
+
```
|
150
|
+
|
151
|
+
2. Client Hydration:
|
152
|
+
```typescript
|
153
|
+
// client.tsx
|
154
|
+
import { hydrate } from 'frontend-hamroun';
|
155
|
+
|
156
|
+
// Type-safe state access
|
157
|
+
declare global {
|
158
|
+
interface Window {
|
159
|
+
__INITIAL_STATE__: {
|
160
|
+
url: string;
|
161
|
+
user?: { id: number; name: string };
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
const { url, user } = window.__INITIAL_STATE__;
|
167
|
+
|
168
|
+
hydrate(<App url={url} user={user} />, document.getElementById('root')!);
|
169
|
+
```
|
170
|
+
|
171
|
+
3. Template Customization:
|
172
|
+
```typescript
|
173
|
+
// Custom template options
|
174
|
+
interface TemplateOptions {
|
175
|
+
title: string;
|
176
|
+
description?: string;
|
177
|
+
scripts?: string[];
|
178
|
+
styles?: string[];
|
179
|
+
meta?: Record<string, string>;
|
180
|
+
initialState?: any;
|
181
|
+
bodyAttrs?: Record<string, string>;
|
182
|
+
htmlAttrs?: Record<string, string>;
|
183
|
+
}
|
184
|
+
```
|
185
|
+
|
186
|
+
### SSR Features
|
187
|
+
|
188
|
+
- ✨ Full HTML document generation
|
189
|
+
- 🔄 Automatic state hydration
|
190
|
+
- 🎨 Customizable templates
|
191
|
+
- 📱 Meta tags for SEO
|
192
|
+
- 🛡️ XSS protection
|
193
|
+
- 🚀 Streaming support
|
194
|
+
- 📦 Asset optimization
|
195
|
+
- 🔍 Error handling
|
196
|
+
|
197
|
+
### Best Practices
|
198
|
+
|
199
|
+
1. State Management:
|
200
|
+
```typescript
|
201
|
+
// Shared types between server and client
|
202
|
+
interface AppState {
|
203
|
+
url: string;
|
204
|
+
user?: UserData;
|
205
|
+
settings: AppSettings;
|
206
|
+
}
|
207
|
+
|
208
|
+
// Server
|
209
|
+
const state: AppState = {
|
210
|
+
url: req.url,
|
211
|
+
user: await getUser(req),
|
212
|
+
settings: await getSettings()
|
213
|
+
};
|
214
|
+
|
215
|
+
// Client
|
216
|
+
const { url, user, settings } = window.__INITIAL_STATE__ as AppState;
|
217
|
+
```
|
218
|
+
|
219
|
+
2. Error Handling:
|
220
|
+
```typescript
|
221
|
+
// Server-side error boundary
|
222
|
+
try {
|
223
|
+
const html = await renderToString(<App />, options);
|
224
|
+
res.send(html);
|
225
|
+
} catch (error) {
|
226
|
+
const errorHtml = await renderToString(
|
227
|
+
<ErrorPage error={error} />,
|
228
|
+
{ title: 'Error' }
|
229
|
+
);
|
230
|
+
res.status(500).send(errorHtml);
|
231
|
+
}
|
232
|
+
```
|
233
|
+
|
234
|
+
3. Performance Optimization:
|
235
|
+
```typescript
|
236
|
+
// Caching
|
237
|
+
const cached = await cache.get(cacheKey);
|
238
|
+
if (cached) return cached;
|
239
|
+
|
240
|
+
const html = await renderToString(<App />, {
|
241
|
+
...options,
|
242
|
+
scripts: [
|
243
|
+
{ src: '/client.js', async: true, defer: true }
|
244
|
+
]
|
245
|
+
});
|
246
|
+
|
247
|
+
await cache.set(cacheKey, html, '1h');
|
248
|
+
```
|
249
|
+
|
250
|
+
### Basic SSR Setup
|
251
|
+
|
252
|
+
```typescript
|
253
|
+
// Server-side rendering with customization
|
254
|
+
const html = await renderToString(<App />, {
|
255
|
+
title: 'My App',
|
256
|
+
description: 'Server-rendered application',
|
257
|
+
scripts: ['/assets/client.js'],
|
258
|
+
styles: ['/assets/styles.css'],
|
259
|
+
meta: {
|
260
|
+
'theme-color': '#ffffff',
|
261
|
+
'og:title': 'My App',
|
262
|
+
'og:description': 'My awesome app'
|
263
|
+
},
|
264
|
+
initialState: {
|
265
|
+
user: { name: 'John' }
|
266
|
+
},
|
267
|
+
bodyAttrs: { class: 'dark-mode' },
|
268
|
+
htmlAttrs: { lang: 'en', dir: 'ltr' }
|
269
|
+
});
|
270
|
+
```
|
271
|
+
|
272
|
+
### Express Server Integration
|
273
|
+
|
274
|
+
```typescript
|
275
|
+
import express from 'express';
|
276
|
+
import { renderToString } from 'frontend-hamroun';
|
277
|
+
|
278
|
+
const app = express();
|
279
|
+
|
280
|
+
app.get('*', async (req, res) => {
|
281
|
+
const initialState = {
|
282
|
+
url: req.url,
|
283
|
+
user: req.user // From your auth middleware
|
284
|
+
};
|
285
|
+
|
286
|
+
const html = await renderToString(<App />, {
|
287
|
+
title: 'My App',
|
288
|
+
scripts: ['/client.js'],
|
289
|
+
initialState
|
290
|
+
});
|
291
|
+
|
292
|
+
res.send(html);
|
293
|
+
});
|
294
|
+
```
|
295
|
+
|
296
|
+
### Client-Side Hydration
|
297
|
+
|
298
|
+
```typescript
|
299
|
+
import { hydrate } from 'frontend-hamroun';
|
300
|
+
|
301
|
+
// Access the server-provided state
|
302
|
+
declare global {
|
303
|
+
interface Window {
|
304
|
+
__INITIAL_STATE__: any;
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
308
|
+
const initialState = window.__INITIAL_STATE__;
|
309
|
+
|
310
|
+
hydrate(
|
311
|
+
<App initialState={initialState} />,
|
312
|
+
document.getElementById('root')!
|
313
|
+
);
|
104
314
|
```
|
105
315
|
|
106
316
|
## CLI Tools
|
@@ -140,6 +350,49 @@ npx create-frontend-app my-app
|
|
140
350
|
- Error boundaries
|
141
351
|
- Performance monitoring
|
142
352
|
|
353
|
+
## Advanced Features
|
354
|
+
|
355
|
+
### Template Customization
|
356
|
+
```typescript
|
357
|
+
interface TemplateOptions {
|
358
|
+
title: string;
|
359
|
+
description?: string;
|
360
|
+
scripts?: string[];
|
361
|
+
styles?: string[];
|
362
|
+
meta?: Record<string, string>;
|
363
|
+
initialState?: any;
|
364
|
+
bodyAttrs?: Record<string, string>;
|
365
|
+
htmlAttrs?: Record<string, string>;
|
366
|
+
}
|
367
|
+
|
368
|
+
// Usage with custom template
|
369
|
+
const html = await renderToString(<App />, {
|
370
|
+
title: 'Custom Template',
|
371
|
+
styles: ['/styles.css'],
|
372
|
+
bodyAttrs: { 'data-theme': 'dark' }
|
373
|
+
});
|
374
|
+
```
|
375
|
+
|
376
|
+
### Error Handling
|
377
|
+
```typescript
|
378
|
+
// Error boundary component
|
379
|
+
function ErrorBoundary({ children }) {
|
380
|
+
const [error, resetError] = useErrorBoundary();
|
381
|
+
|
382
|
+
if (error) {
|
383
|
+
return (
|
384
|
+
<div role="alert">
|
385
|
+
<h2>Something went wrong</h2>
|
386
|
+
<pre>{error.message}</pre>
|
387
|
+
<button onClick={resetError}>Try again</button>
|
388
|
+
</div>
|
389
|
+
);
|
390
|
+
}
|
391
|
+
|
392
|
+
return children;
|
393
|
+
}
|
394
|
+
```
|
395
|
+
|
143
396
|
## Contributing
|
144
397
|
|
145
398
|
1. Fork the repository
|
package/package.json
CHANGED
@@ -1,26 +1,36 @@
|
|
1
|
-
import { useState } from 'frontend-hamroun';
|
1
|
+
import { useState, useEffect } from 'frontend-hamroun';
|
2
|
+
|
3
|
+
interface AppProps {
|
4
|
+
initialUrl?: string;
|
5
|
+
initialSettings?: {
|
6
|
+
theme: string;
|
7
|
+
};
|
8
|
+
user?: any;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function App({
|
12
|
+
initialUrl = '/',
|
13
|
+
initialSettings = { theme: 'light' },
|
14
|
+
user = null
|
15
|
+
}: AppProps) {
|
16
|
+
const [theme, setTheme] = useState(initialSettings.theme);
|
17
|
+
|
18
|
+
useEffect(() => {
|
19
|
+
document.body.dataset.theme = theme;
|
20
|
+
}, [theme]);
|
2
21
|
|
3
|
-
export function App() {
|
4
|
-
// Initialize with a default state that works on both server and client
|
5
|
-
const [count, setCount] = useState(0);
|
6
|
-
|
7
22
|
return (
|
8
|
-
<div>
|
9
|
-
<
|
10
|
-
|
11
|
-
<button
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
<
|
17
|
-
|
18
|
-
|
19
|
-
>+</button>
|
20
|
-
</div>
|
21
|
-
<script dangerouslySetInnerHTML={{
|
22
|
-
__html: `window.__INITIAL_STATE__ = ${JSON.stringify({ count: 0 })};`
|
23
|
-
}} />
|
23
|
+
<div className="app">
|
24
|
+
<header>
|
25
|
+
<h1>My SSR App</h1>
|
26
|
+
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
|
27
|
+
Toggle Theme
|
28
|
+
</button>
|
29
|
+
</header>
|
30
|
+
<main>
|
31
|
+
<p>Current URL: {initialUrl}</p>
|
32
|
+
{user && <p>Welcome, {user.name}!</p>}
|
33
|
+
</main>
|
24
34
|
</div>
|
25
35
|
);
|
26
36
|
}
|
@@ -1,11 +1,29 @@
|
|
1
1
|
import { hydrate } from 'frontend-hamroun';
|
2
2
|
import { App } from './App';
|
3
3
|
|
4
|
+
// Type-safe initial state access
|
5
|
+
declare global {
|
6
|
+
interface Window {
|
7
|
+
__INITIAL_STATE__: {
|
8
|
+
url: string;
|
9
|
+
user: any;
|
10
|
+
settings: {
|
11
|
+
theme: string;
|
12
|
+
};
|
13
|
+
};
|
14
|
+
}
|
15
|
+
}
|
4
16
|
|
17
|
+
// Get state from server
|
18
|
+
const initialState = window.__INITIAL_STATE__;
|
5
19
|
|
6
20
|
document.addEventListener('DOMContentLoaded', () => {
|
7
21
|
hydrate(
|
8
|
-
<App
|
22
|
+
<App
|
23
|
+
initialUrl={initialState.url}
|
24
|
+
initialSettings={initialState.settings}
|
25
|
+
user={initialState.user}
|
26
|
+
/>,
|
9
27
|
document.getElementById('root')!
|
10
28
|
);
|
11
29
|
});
|
@@ -1,43 +1,38 @@
|
|
1
1
|
import express from 'express';
|
2
2
|
import path from 'path';
|
3
3
|
import { fileURLToPath } from 'url';
|
4
|
-
import { renderToString
|
5
|
-
import { App } from './App
|
6
|
-
import fs from 'fs';
|
7
|
-
import { title } from 'process';
|
4
|
+
import { renderToString } from 'frontend-hamroun';
|
5
|
+
import { App } from './App';
|
8
6
|
|
9
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
10
8
|
const app = express();
|
11
9
|
const port = 3000;
|
12
10
|
|
13
|
-
//
|
14
|
-
|
15
|
-
const assetsDir = path.join(__dirname, './');
|
16
|
-
const files = fs.readdirSync(assetsDir);
|
17
|
-
const entry = files.find(file => file.startsWith("client") && file.endsWith('.js'));
|
18
|
-
if (!entry) {
|
19
|
-
throw new Error('Client entry file not found');
|
20
|
-
}
|
21
|
-
return entry;
|
22
|
-
}
|
23
|
-
|
24
|
-
// Static file serving
|
25
|
-
app.use('/assets', express.static(path.join(__dirname, './')));
|
26
|
-
app.use(express.static(path.join(__dirname, 'dist')));
|
11
|
+
// Static files
|
12
|
+
app.use('/assets', express.static(path.join(__dirname, './assets')));
|
27
13
|
|
28
|
-
//
|
29
|
-
app.get('
|
14
|
+
// All routes handler
|
15
|
+
app.get('*', async (req, res) => {
|
30
16
|
try {
|
31
|
-
|
32
|
-
const
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
17
|
+
// Get initial state based on route
|
18
|
+
const initialState = {
|
19
|
+
url: req.url,
|
20
|
+
user: null, // Add your user data here
|
21
|
+
settings: { theme: 'light' }
|
22
|
+
};
|
23
|
+
|
24
|
+
const html = await renderToString(<App />, {
|
25
|
+
title: 'My SSR App',
|
26
|
+
description: 'Server-side rendered application',
|
27
|
+
scripts: ['/assets/client.js'],
|
28
|
+
styles: ['/assets/styles.css'],
|
29
|
+
initialState,
|
39
30
|
meta: {
|
40
|
-
'
|
31
|
+
'theme-color': '#ffffff',
|
32
|
+
'og:title': 'My SSR App'
|
33
|
+
},
|
34
|
+
bodyAttrs: {
|
35
|
+
'data-theme': initialState.settings.theme
|
41
36
|
}
|
42
37
|
});
|
43
38
|
|
@@ -49,5 +44,5 @@ app.get('/', async (_req: any, res: { send: (arg0: string) => void; status: (arg
|
|
49
44
|
});
|
50
45
|
|
51
46
|
app.listen(port, () => {
|
52
|
-
console.log(
|
47
|
+
console.log(`✓ Server running at http://localhost:${port}`);
|
53
48
|
});
|