frontend-hamroun 1.2.77 → 1.2.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/batch/package.json +1 -1
- package/dist/client-router/package.json +1 -1
- package/dist/component/package.json +1 -1
- package/dist/context/package.json +1 -1
- package/dist/event-bus/package.json +1 -1
- package/dist/forms/package.json +1 -1
- package/dist/hooks/package.json +1 -1
- package/dist/index.mjs +1 -0
- package/dist/jsx-runtime/package.json +1 -1
- package/dist/lifecycle-events/package.json +1 -1
- package/dist/package.json +1 -1
- package/dist/render-component/package.json +1 -1
- package/dist/renderer/package.json +1 -1
- package/dist/router/package.json +1 -1
- package/dist/server/package.json +1 -1
- package/dist/server/src/index.js +24 -23
- package/dist/server/src/index.js.map +1 -1
- package/dist/server/src/renderComponent.d.ts +8 -9
- package/dist/server/src/renderComponent.js +10 -5
- package/dist/server/src/renderComponent.js.map +1 -1
- package/dist/server/src/server/index.d.ts +23 -34
- package/dist/server/src/server/index.js +170 -50
- package/dist/server/src/server/index.js.map +1 -1
- package/dist/server/src/server/templates.d.ts +2 -0
- package/dist/server/src/server/templates.js +9 -5
- package/dist/server/src/server/templates.js.map +1 -1
- package/dist/server/src/server/utils.d.ts +1 -1
- package/dist/server/src/server/utils.js +1 -1
- package/dist/server/src/server/utils.js.map +1 -1
- package/dist/server/tsconfig.server.tsbuildinfo +1 -1
- package/dist/server-renderer/package.json +1 -1
- package/dist/store/package.json +1 -1
- package/dist/types/package.json +1 -1
- package/dist/utils/package.json +1 -1
- package/dist/vdom/package.json +1 -1
- package/dist/wasm/package.json +1 -1
- package/package.json +1 -1
- package/templates/complete-app/client.js +58 -0
- package/templates/complete-app/package-lock.json +2536 -0
- package/templates/complete-app/package.json +8 -31
- package/templates/complete-app/pages/about.js +119 -0
- package/templates/complete-app/pages/index.js +157 -0
- package/templates/complete-app/pages/wasm-demo.js +290 -0
- package/templates/complete-app/public/client.js +80 -0
- package/templates/complete-app/public/index.html +47 -0
- package/templates/complete-app/public/styles.css +446 -212
- package/templates/complete-app/readme.md +188 -0
- package/templates/complete-app/server.js +417 -0
- package/templates/complete-app/server.ts +275 -0
- package/templates/complete-app/src/App.tsx +59 -0
- package/templates/complete-app/src/client.ts +61 -0
- package/templates/complete-app/src/client.tsx +18 -0
- package/templates/complete-app/src/pages/index.tsx +51 -0
- package/templates/complete-app/src/server.ts +218 -0
- package/templates/complete-app/tsconfig.json +22 -0
- package/templates/complete-app/tsconfig.server.json +19 -0
- package/templates/complete-app/vite.config.js +57 -0
- package/templates/complete-app/vite.config.ts +30 -0
- package/templates/go/example.go +154 -99
- package/templates/complete-app/build.js +0 -284
- package/templates/complete-app/src/api/index.js +0 -31
- package/templates/complete-app/src/client.js +0 -93
- package/templates/complete-app/src/components/App.js +0 -66
- package/templates/complete-app/src/components/Footer.js +0 -19
- package/templates/complete-app/src/components/Header.js +0 -38
- package/templates/complete-app/src/pages/About.js +0 -59
- package/templates/complete-app/src/pages/Home.js +0 -54
- package/templates/complete-app/src/pages/WasmDemo.js +0 -136
- package/templates/complete-app/src/server.js +0 -186
- package/templates/complete-app/src/wasm/build.bat +0 -16
- package/templates/complete-app/src/wasm/build.sh +0 -16
- package/templates/complete-app/src/wasm/example.go +0 -101
@@ -0,0 +1,275 @@
|
|
1
|
+
import express from 'express';
|
2
|
+
import { fileURLToPath } from 'url';
|
3
|
+
import { dirname, join } from 'path';
|
4
|
+
import { renderToString } from 'frontend-hamroun';
|
5
|
+
import { Database } from 'frontend-hamroun';
|
6
|
+
import { AuthService } from 'frontend-hamroun';
|
7
|
+
import { requestLogger, errorHandler, notFoundHandler, rateLimit } from 'frontend-hamroun';
|
8
|
+
import dotenv from 'dotenv';
|
9
|
+
import fetch from 'node-fetch';
|
10
|
+
|
11
|
+
// Load environment variables
|
12
|
+
dotenv.config();
|
13
|
+
|
14
|
+
// Get directory name in ESM
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
16
|
+
const __dirname = dirname(__filename);
|
17
|
+
|
18
|
+
// Create Express app
|
19
|
+
const app = express();
|
20
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
21
|
+
|
22
|
+
// Add middleware
|
23
|
+
app.use(express.json());
|
24
|
+
app.use(express.urlencoded({ extended: true }));
|
25
|
+
app.use(requestLogger);
|
26
|
+
|
27
|
+
// Rate limiting for API routes
|
28
|
+
app.use('/api', rateLimit({
|
29
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
30
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
31
|
+
}));
|
32
|
+
|
33
|
+
// Configure database if connection string is provided
|
34
|
+
let db = null;
|
35
|
+
if (process.env.DATABASE_URL) {
|
36
|
+
db = new Database({
|
37
|
+
url: process.env.DATABASE_URL,
|
38
|
+
type: (process.env.DATABASE_TYPE || 'mongodb') as 'mongodb' | 'mysql' | 'postgres'
|
39
|
+
});
|
40
|
+
|
41
|
+
// Connect to database
|
42
|
+
try {
|
43
|
+
await db.connect();
|
44
|
+
console.log('Database connected successfully');
|
45
|
+
} catch (error) {
|
46
|
+
console.error('Database connection failed:', error);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// Configure auth if secret is provided
|
51
|
+
let auth = null;
|
52
|
+
if (process.env.JWT_SECRET) {
|
53
|
+
auth = new AuthService({
|
54
|
+
secret: process.env.JWT_SECRET,
|
55
|
+
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
|
56
|
+
});
|
57
|
+
|
58
|
+
// Add auth middleware
|
59
|
+
app.use(auth.initialize());
|
60
|
+
|
61
|
+
// Example protected route
|
62
|
+
app.get('/api/protected', auth.requireAuth(), (req, res) => {
|
63
|
+
res.json({ message: 'Protected route accessed successfully' });
|
64
|
+
});
|
65
|
+
|
66
|
+
// Example role-based protection
|
67
|
+
app.get('/api/admin', auth.requireRoles(['admin']), (req, res) => {
|
68
|
+
res.json({ message: 'Admin route accessed successfully' });
|
69
|
+
});
|
70
|
+
|
71
|
+
// Login route
|
72
|
+
app.post('/api/login', async (req, res) => {
|
73
|
+
const { username, password } = req.body;
|
74
|
+
|
75
|
+
// In a real app, fetch user from database
|
76
|
+
const user = { id: 1, username, roles: ['user'] };
|
77
|
+
const token = auth.generateToken(user);
|
78
|
+
|
79
|
+
res.json({ token, user: { id: user.id, username: user.username, roles: user.roles } });
|
80
|
+
});
|
81
|
+
}
|
82
|
+
|
83
|
+
// Serve static files from public directory
|
84
|
+
app.use(express.static(join(__dirname, 'public')));
|
85
|
+
|
86
|
+
// API endpoint example
|
87
|
+
app.get('/api/page-data', (req, res) => {
|
88
|
+
res.json({
|
89
|
+
title: 'Server-side Data',
|
90
|
+
content: 'This data was fetched from the server',
|
91
|
+
timestamp: new Date().toISOString()
|
92
|
+
});
|
93
|
+
});
|
94
|
+
|
95
|
+
// Meta tag generation function (using local logic for simplicity)
|
96
|
+
async function generateMetaTags(pageContent) {
|
97
|
+
// Extract title from page content
|
98
|
+
const title = pageContent.split('\n')[0].replace(/[#*]/g, '').trim() ||
|
99
|
+
'Frontend Hamroun SSR Page';
|
100
|
+
|
101
|
+
// Generate description from content
|
102
|
+
const description = pageContent.substring(0, 150) + '...';
|
103
|
+
|
104
|
+
// Extract keywords
|
105
|
+
const keywords = pageContent
|
106
|
+
.toLowerCase()
|
107
|
+
.replace(/[^\w\s]/g, '')
|
108
|
+
.split(/\s+/)
|
109
|
+
.filter(w => w.length > 3)
|
110
|
+
.slice(0, 5)
|
111
|
+
.join(', ');
|
112
|
+
|
113
|
+
return {
|
114
|
+
title,
|
115
|
+
description,
|
116
|
+
keywords
|
117
|
+
};
|
118
|
+
}
|
119
|
+
|
120
|
+
// Helper function to check if file exists
|
121
|
+
async function fileExists(path) {
|
122
|
+
try {
|
123
|
+
const fs = await import('fs/promises');
|
124
|
+
await fs.access(path);
|
125
|
+
return true;
|
126
|
+
} catch {
|
127
|
+
return false;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
// Implement basic SSR without relying on complex server functionality
|
132
|
+
app.get('*', async (req, res) => {
|
133
|
+
try {
|
134
|
+
// Import the page component
|
135
|
+
const pagesDir = join(__dirname, 'src', 'pages');
|
136
|
+
let componentPath;
|
137
|
+
|
138
|
+
// Map URL path to component file
|
139
|
+
if (req.path === '/') {
|
140
|
+
componentPath = join(pagesDir, 'index.js');
|
141
|
+
} else {
|
142
|
+
componentPath = join(pagesDir, `${req.path}.js`);
|
143
|
+
// Check if it's a directory with index.js
|
144
|
+
if (!await fileExists(componentPath)) {
|
145
|
+
componentPath = join(pagesDir, req.path, 'index.js');
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
// If component doesn't exist, return 404
|
150
|
+
if (!await fileExists(componentPath)) {
|
151
|
+
return res.status(404).send(`
|
152
|
+
<!DOCTYPE html>
|
153
|
+
<html>
|
154
|
+
<head>
|
155
|
+
<title>404 - Page Not Found</title>
|
156
|
+
</head>
|
157
|
+
<body>
|
158
|
+
<h1>404 - Page Not Found</h1>
|
159
|
+
<p>The page you requested does not exist.</p>
|
160
|
+
</body>
|
161
|
+
</html>
|
162
|
+
`);
|
163
|
+
}
|
164
|
+
|
165
|
+
// Import the component
|
166
|
+
const { default: PageComponent } = await import(componentPath);
|
167
|
+
|
168
|
+
// Generate page content for meta tags
|
169
|
+
const pageContent = `
|
170
|
+
Frontend Hamroun SSR Page
|
171
|
+
This is a server-rendered page using the Frontend Hamroun framework.
|
172
|
+
Path: ${req.path}
|
173
|
+
Timestamp: ${new Date().toISOString()}
|
174
|
+
`;
|
175
|
+
|
176
|
+
// Generate meta tags
|
177
|
+
const metaTags = await generateMetaTags(pageContent);
|
178
|
+
|
179
|
+
// Render the component to string
|
180
|
+
const content = renderToString(PageComponent());
|
181
|
+
|
182
|
+
// Send the HTML response
|
183
|
+
res.send(`
|
184
|
+
<!DOCTYPE html>
|
185
|
+
<html lang="en">
|
186
|
+
<head>
|
187
|
+
<meta charset="UTF-8">
|
188
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
189
|
+
|
190
|
+
<!-- Generated Meta Tags -->
|
191
|
+
<title>${metaTags.title}</title>
|
192
|
+
<meta name="description" content="${metaTags.description}">
|
193
|
+
<meta name="keywords" content="${metaTags.keywords}">
|
194
|
+
|
195
|
+
<!-- Open Graph Meta Tags -->
|
196
|
+
<meta property="og:title" content="${metaTags.title}">
|
197
|
+
<meta property="og:description" content="${metaTags.description}">
|
198
|
+
<meta property="og:type" content="website">
|
199
|
+
<meta property="og:url" content="${req.protocol}://${req.get('host')}${req.originalUrl}">
|
200
|
+
|
201
|
+
<!-- Import Tailwind-like styles for quick styling -->
|
202
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.7.4/dist/full.css" rel="stylesheet" type="text/css" />
|
203
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
204
|
+
|
205
|
+
<!-- Client-side script for hydration -->
|
206
|
+
<script type="module" src="/assets/client.js"></script>
|
207
|
+
</head>
|
208
|
+
<body>
|
209
|
+
<div id="app">${content}</div>
|
210
|
+
|
211
|
+
<!-- Add initial state for hydration -->
|
212
|
+
<script>
|
213
|
+
window.__INITIAL_STATE__ = ${JSON.stringify({
|
214
|
+
path: req.path,
|
215
|
+
timestamp: new Date().toISOString(),
|
216
|
+
metaTags
|
217
|
+
})};
|
218
|
+
</script>
|
219
|
+
</body>
|
220
|
+
</html>
|
221
|
+
`);
|
222
|
+
} catch (error) {
|
223
|
+
console.error('Error rendering page:', error);
|
224
|
+
res.status(500).send(`
|
225
|
+
<!DOCTYPE html>
|
226
|
+
<html>
|
227
|
+
<head>
|
228
|
+
<title>500 - Server Error</title>
|
229
|
+
</head>
|
230
|
+
<body>
|
231
|
+
<h1>500 - Server Error</h1>
|
232
|
+
<p>There was an error processing your request.</p>
|
233
|
+
${process.env.NODE_ENV === 'development' ? `<pre>${error.stack}</pre>` : ''}
|
234
|
+
</body>
|
235
|
+
</html>
|
236
|
+
`);
|
237
|
+
}
|
238
|
+
});
|
239
|
+
|
240
|
+
// Add error handler middleware
|
241
|
+
app.use(errorHandler);
|
242
|
+
|
243
|
+
// Add not found handler for API routes that weren't caught
|
244
|
+
app.use(notFoundHandler);
|
245
|
+
|
246
|
+
// Graceful shutdown function to close database connections
|
247
|
+
function gracefulShutdown() {
|
248
|
+
console.log('Shutting down server...');
|
249
|
+
|
250
|
+
// Close database connection if it exists
|
251
|
+
if (db) {
|
252
|
+
db.disconnect()
|
253
|
+
.then(() => console.log('Database disconnected'))
|
254
|
+
.catch(err => console.error('Error disconnecting from database:', err))
|
255
|
+
.finally(() => process.exit(0));
|
256
|
+
} else {
|
257
|
+
process.exit(0);
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
// Start the server
|
262
|
+
const server = app.listen(port, () => {
|
263
|
+
console.log(`Server running at http://localhost:${port}`);
|
264
|
+
console.log(`Available API endpoints:`);
|
265
|
+
console.log(` - GET /api/page-data`);
|
266
|
+
console.log(` - POST /api/login`);
|
267
|
+
if (process.env.JWT_SECRET) {
|
268
|
+
console.log(` - GET /api/protected (requires authentication)`);
|
269
|
+
console.log(` - GET /api/admin (requires admin role)`);
|
270
|
+
}
|
271
|
+
});
|
272
|
+
|
273
|
+
// Handle graceful shutdown
|
274
|
+
process.on('SIGINT', gracefulShutdown);
|
275
|
+
process.on('SIGTERM', gracefulShutdown);
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import { useState, useEffect, useMemo, useRef, createContext } from 'frontend-hamroun';
|
2
|
+
|
3
|
+
// Create a theme context
|
4
|
+
const ThemeContext = createContext('light');
|
5
|
+
|
6
|
+
export function App() {
|
7
|
+
// Initialize with a default state that works on both server and client
|
8
|
+
const [count, setCount] = useState(0);
|
9
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
10
|
+
const renderCount = useRef(0);
|
11
|
+
|
12
|
+
// Client-side only effect
|
13
|
+
useEffect(() => {
|
14
|
+
if (typeof window !== 'undefined') {
|
15
|
+
renderCount.current += 1;
|
16
|
+
console.log('Component rendered', renderCount.current, 'times');
|
17
|
+
}
|
18
|
+
return () => console.log('Component unmounting');
|
19
|
+
}, [count]);
|
20
|
+
|
21
|
+
// Memoized value
|
22
|
+
const doubled = useMemo(() => count * 2, [count]);
|
23
|
+
|
24
|
+
return (
|
25
|
+
<ThemeContext.Provider value={theme}>
|
26
|
+
<div style={{
|
27
|
+
padding: '20px',
|
28
|
+
backgroundColor: theme === 'dark' ? '#333' : '#fff',
|
29
|
+
color: theme === 'dark' ? '#fff' : '#333'
|
30
|
+
}}>
|
31
|
+
<h1>Server-Side Rendered App</h1>
|
32
|
+
<div>
|
33
|
+
<button
|
34
|
+
onClick={() => setCount(count - 1)}
|
35
|
+
data-action="decrement"
|
36
|
+
>-</button>
|
37
|
+
<span style={{ margin: '0 10px' }}>{count}</span>
|
38
|
+
<button
|
39
|
+
onClick={() => setCount(count + 1)}
|
40
|
+
data-action="increment"
|
41
|
+
>+</button>
|
42
|
+
</div>
|
43
|
+
<p>Doubled value: {doubled}</p>
|
44
|
+
{typeof window !== 'undefined' && (
|
45
|
+
<p>Render count: {renderCount.current}</p>
|
46
|
+
)}
|
47
|
+
<button
|
48
|
+
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
49
|
+
style={{ marginTop: '10px' }}
|
50
|
+
>
|
51
|
+
Toggle Theme ({theme})
|
52
|
+
</button>
|
53
|
+
<script dangerouslySetInnerHTML={{
|
54
|
+
__html: `window.__INITIAL_STATE__ = ${JSON.stringify({ count: 0, theme: 'light' })};`
|
55
|
+
}} />
|
56
|
+
</div>
|
57
|
+
</ThemeContext.Provider>
|
58
|
+
);
|
59
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { hydrate, jsx } from 'frontend-hamroun';
|
2
|
+
|
3
|
+
// Dynamically import the appropriate page component
|
4
|
+
async function hydratePage() {
|
5
|
+
try {
|
6
|
+
// Get initial state from server
|
7
|
+
const initialState = window.__INITIAL_STATE__ || {};
|
8
|
+
|
9
|
+
// Get current path
|
10
|
+
const path = initialState.route || window.location.pathname;
|
11
|
+
const normalizedPath = path === '/' ? '/index' : path;
|
12
|
+
|
13
|
+
// Create path to module
|
14
|
+
const modulePath = `.${normalizedPath.replace(/\/$/, '')}.js`;
|
15
|
+
|
16
|
+
try {
|
17
|
+
// Dynamically import the component
|
18
|
+
const module = await import(`./pages${normalizedPath}.js`).catch(() =>
|
19
|
+
import(`./pages${normalizedPath}/index.js`));
|
20
|
+
|
21
|
+
const PageComponent = module.default;
|
22
|
+
|
23
|
+
// Find the root element
|
24
|
+
const rootElement = document.getElementById('root');
|
25
|
+
|
26
|
+
if (rootElement && PageComponent) {
|
27
|
+
// Hydrate the application with the same params from the server
|
28
|
+
hydrate(jsx(PageComponent, { params: initialState.params || {} }), rootElement);
|
29
|
+
console.log('Hydration complete');
|
30
|
+
} else {
|
31
|
+
console.error('Could not find root element or page component');
|
32
|
+
}
|
33
|
+
} catch (importError) {
|
34
|
+
console.error('Error importing page component:', importError);
|
35
|
+
|
36
|
+
// Fallback to App component if available
|
37
|
+
try {
|
38
|
+
const { App } = await import('./App.js');
|
39
|
+
const rootElement = document.getElementById('root');
|
40
|
+
|
41
|
+
if (rootElement && App) {
|
42
|
+
hydrate(jsx(App, {}), rootElement);
|
43
|
+
console.log('Fallback hydration complete');
|
44
|
+
}
|
45
|
+
} catch (fallbackError) {
|
46
|
+
console.error('Fallback hydration failed:', fallbackError);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
} catch (error) {
|
50
|
+
console.error('Hydration error:', error);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
// Add global variable for JSX
|
55
|
+
window.jsx = jsx;
|
56
|
+
|
57
|
+
// Hydrate when DOM is ready
|
58
|
+
document.addEventListener('DOMContentLoaded', hydratePage);
|
59
|
+
|
60
|
+
// Handle client-side navigation (if implemented)
|
61
|
+
window.addEventListener('popstate', hydratePage);
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { hydrate, createElement } from 'frontend-hamroun';
|
2
|
+
|
3
|
+
// For simplicity in this example, we just hydrate the root component
|
4
|
+
// In a more complex app, you might use a router
|
5
|
+
import HomePage from './pages/index';
|
6
|
+
|
7
|
+
// When the DOM is ready, hydrate the server-rendered HTML
|
8
|
+
document.addEventListener('DOMContentLoaded', () => {
|
9
|
+
const rootElement = document.getElementById('app');
|
10
|
+
|
11
|
+
if (rootElement) {
|
12
|
+
// Hydrate the app with the same component that was rendered on the server
|
13
|
+
hydrate(<HomePage />, rootElement);
|
14
|
+
console.log('Hydration complete');
|
15
|
+
} else {
|
16
|
+
console.error('Could not find root element with id "app"');
|
17
|
+
}
|
18
|
+
});
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { useState, useEffect } from 'frontend-hamroun';
|
2
|
+
|
3
|
+
// This component will be rendered on both server and client
|
4
|
+
export default function HomePage() {
|
5
|
+
const [count, setCount] = useState(0);
|
6
|
+
const [serverTime, setServerTime] = useState('');
|
7
|
+
|
8
|
+
// This effect only runs on the client after hydration
|
9
|
+
useEffect(() => {
|
10
|
+
// Set server render time (only when hydrating)
|
11
|
+
if (!serverTime) {
|
12
|
+
setServerTime('Client-side hydration complete');
|
13
|
+
}
|
14
|
+
|
15
|
+
// Simple cleanup function
|
16
|
+
return () => {
|
17
|
+
console.log('Component unmounting');
|
18
|
+
};
|
19
|
+
}, []);
|
20
|
+
|
21
|
+
return (
|
22
|
+
<div id="app">
|
23
|
+
<div className="hero min-h-screen bg-base-200">
|
24
|
+
<div className="hero-content text-center">
|
25
|
+
<div className="max-w-md">
|
26
|
+
<h1 className="text-5xl font-bold">Frontend Hamroun SSR</h1>
|
27
|
+
<p className="py-6">
|
28
|
+
This page was rendered on the server and hydrated on the client.
|
29
|
+
</p>
|
30
|
+
|
31
|
+
{/* Interactive counter demonstrates client-side hydration */}
|
32
|
+
<div className="my-4 p-4 bg-base-300 rounded-lg">
|
33
|
+
<p>Counter: {count}</p>
|
34
|
+
<button
|
35
|
+
className="btn btn-primary mt-2"
|
36
|
+
onClick={() => setCount(count + 1)}
|
37
|
+
>
|
38
|
+
Increment
|
39
|
+
</button>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
{/* Shows server render time or hydration status */}
|
43
|
+
<div className="text-sm opacity-70 mt-4">
|
44
|
+
{serverTime ? serverTime : `Server rendered at: ${new Date().toISOString()}`}
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
}
|
@@ -0,0 +1,218 @@
|
|
1
|
+
import express from 'express';
|
2
|
+
import path from 'path';
|
3
|
+
import { fileURLToPath } from 'url';
|
4
|
+
import { renderToString, jsx } from 'frontend-hamroun';
|
5
|
+
import { App } from './App.js';
|
6
|
+
import fs from 'fs';
|
7
|
+
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
9
|
+
const app = express();
|
10
|
+
const port = 3000;
|
11
|
+
|
12
|
+
// Find the client entry file
|
13
|
+
const getClientEntry = () => {
|
14
|
+
const assetsDir = path.join(__dirname, './');
|
15
|
+
const files = fs.readdirSync(assetsDir);
|
16
|
+
return files.find(file => file.startsWith("client") && file.endsWith('.js'));
|
17
|
+
};
|
18
|
+
|
19
|
+
// Serve static files from dist/assets
|
20
|
+
app.use('/assets', express.static(path.join(__dirname, './')));
|
21
|
+
|
22
|
+
// Serve static files from dist
|
23
|
+
app.use(express.static(path.join(__dirname, 'dist')));
|
24
|
+
|
25
|
+
// Auto-routing middleware - scans the pages directory for components
|
26
|
+
const getPagesDirectory = () => {
|
27
|
+
return path.join(__dirname, 'pages');
|
28
|
+
};
|
29
|
+
|
30
|
+
// Helper to check if a file exists
|
31
|
+
const fileExists = async (filePath) => {
|
32
|
+
try {
|
33
|
+
await fs.promises.access(filePath);
|
34
|
+
return true;
|
35
|
+
} catch {
|
36
|
+
return false;
|
37
|
+
}
|
38
|
+
};
|
39
|
+
|
40
|
+
// Map URL path to component path
|
41
|
+
const getComponentPath = async (urlPath) => {
|
42
|
+
const pagesDir = getPagesDirectory();
|
43
|
+
|
44
|
+
// Handle root path
|
45
|
+
if (urlPath === '/') {
|
46
|
+
const indexPath = path.join(pagesDir, 'index.js');
|
47
|
+
if (await fileExists(indexPath)) {
|
48
|
+
return {
|
49
|
+
componentPath: indexPath,
|
50
|
+
params: {}
|
51
|
+
};
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
// Try direct match (e.g., /about -> /pages/about.js)
|
56
|
+
const directPath = path.join(pagesDir, `${urlPath.slice(1)}.js`);
|
57
|
+
if (await fileExists(directPath)) {
|
58
|
+
return {
|
59
|
+
componentPath: directPath,
|
60
|
+
params: {}
|
61
|
+
};
|
62
|
+
}
|
63
|
+
|
64
|
+
// Try directory index (e.g., /about -> /pages/about/index.js)
|
65
|
+
const dirIndexPath = path.join(pagesDir, urlPath.slice(1), 'index.js');
|
66
|
+
if (await fileExists(dirIndexPath)) {
|
67
|
+
return {
|
68
|
+
componentPath: dirIndexPath,
|
69
|
+
params: {}
|
70
|
+
};
|
71
|
+
}
|
72
|
+
|
73
|
+
// Look for dynamic routes (with [param] in filename)
|
74
|
+
const segments = urlPath.split('/').filter(Boolean);
|
75
|
+
const dynamicRoutes = [];
|
76
|
+
|
77
|
+
// Recursively scan pages directory for all files
|
78
|
+
const scanDir = (dir, basePath = '') => {
|
79
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
80
|
+
|
81
|
+
for (const item of items) {
|
82
|
+
const itemPath = path.join(dir, item.name);
|
83
|
+
const routePath = path.join(basePath, item.name);
|
84
|
+
|
85
|
+
if (item.isDirectory()) {
|
86
|
+
scanDir(itemPath, routePath);
|
87
|
+
} else if (item.name.endsWith('.js') && item.name.includes('[')) {
|
88
|
+
// This is a dynamic route file
|
89
|
+
const urlPattern = routePath
|
90
|
+
.replace(/\.js$/, '')
|
91
|
+
.replace(/\[([^\]]+)\]/g, ':$1');
|
92
|
+
|
93
|
+
dynamicRoutes.push({
|
94
|
+
pattern: urlPattern,
|
95
|
+
componentPath: itemPath
|
96
|
+
});
|
97
|
+
}
|
98
|
+
}
|
99
|
+
};
|
100
|
+
|
101
|
+
scanDir(pagesDir);
|
102
|
+
|
103
|
+
// Check if any dynamic routes match
|
104
|
+
for (const route of dynamicRoutes) {
|
105
|
+
const routeSegments = route.pattern.split('/').filter(Boolean);
|
106
|
+
if (routeSegments.length !== segments.length) continue;
|
107
|
+
|
108
|
+
const params = {};
|
109
|
+
let matches = true;
|
110
|
+
|
111
|
+
for (let i = 0; i < segments.length; i++) {
|
112
|
+
const routeSeg = routeSegments[i];
|
113
|
+
const urlSeg = segments[i];
|
114
|
+
|
115
|
+
if (routeSeg.startsWith(':')) {
|
116
|
+
// This is a parameter
|
117
|
+
const paramName = routeSeg.slice(1);
|
118
|
+
params[paramName] = urlSeg;
|
119
|
+
} else if (routeSeg !== urlSeg) {
|
120
|
+
matches = false;
|
121
|
+
break;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
if (matches) {
|
126
|
+
return {
|
127
|
+
componentPath: route.componentPath,
|
128
|
+
params
|
129
|
+
};
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
// No match found
|
134
|
+
return null;
|
135
|
+
};
|
136
|
+
|
137
|
+
// Handle all routes with auto-routing
|
138
|
+
app.get('*', async (req, res) => {
|
139
|
+
try {
|
140
|
+
const clientEntry = getClientEntry();
|
141
|
+
const routeResult = await getComponentPath(req.path);
|
142
|
+
|
143
|
+
if (!routeResult) {
|
144
|
+
return res.status(404).send(`
|
145
|
+
<!DOCTYPE html>
|
146
|
+
<html>
|
147
|
+
<head>
|
148
|
+
<title>404 - Page Not Found</title>
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
150
|
+
</head>
|
151
|
+
<body>
|
152
|
+
<div id="root">
|
153
|
+
<h1>404 - Page Not Found</h1>
|
154
|
+
<p>The page you requested could not be found.</p>
|
155
|
+
</div>
|
156
|
+
<script type="module" src="/assets/${clientEntry}"></script>
|
157
|
+
</body>
|
158
|
+
</html>
|
159
|
+
`);
|
160
|
+
}
|
161
|
+
|
162
|
+
// Import the component dynamically
|
163
|
+
const { default: PageComponent } = await import(routeResult.componentPath);
|
164
|
+
|
165
|
+
if (!PageComponent) {
|
166
|
+
throw new Error(`Invalid component in ${routeResult.componentPath}`);
|
167
|
+
}
|
168
|
+
|
169
|
+
// Render the component with params
|
170
|
+
const html = await renderToString(jsx(PageComponent, { params: routeResult.params }));
|
171
|
+
|
172
|
+
// Create route data for client hydration
|
173
|
+
const initialState = {
|
174
|
+
route: req.path,
|
175
|
+
params: routeResult.params,
|
176
|
+
timestamp: new Date().toISOString()
|
177
|
+
};
|
178
|
+
|
179
|
+
res.send(`
|
180
|
+
<!DOCTYPE html>
|
181
|
+
<html>
|
182
|
+
<head>
|
183
|
+
<title>Frontend Hamroun SSR</title>
|
184
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
185
|
+
<meta charset="UTF-8">
|
186
|
+
</head>
|
187
|
+
<body>
|
188
|
+
<div id="root">${html}</div>
|
189
|
+
<script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script>
|
190
|
+
<script type="module" src="/assets/${clientEntry}"></script>
|
191
|
+
</body>
|
192
|
+
</html>
|
193
|
+
`);
|
194
|
+
} catch (error) {
|
195
|
+
console.error('Rendering error:', error);
|
196
|
+
res.status(500).send(`
|
197
|
+
<!DOCTYPE html>
|
198
|
+
<html>
|
199
|
+
<head>
|
200
|
+
<title>500 - Server Error</title>
|
201
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
202
|
+
</head>
|
203
|
+
<body>
|
204
|
+
<div id="root">
|
205
|
+
<h1>500 - Server Error</h1>
|
206
|
+
<p>Something went wrong on the server.</p>
|
207
|
+
<pre>${process.env.NODE_ENV === 'production' ? '' : error.stack}</pre>
|
208
|
+
</div>
|
209
|
+
<script type="module" src="/assets/${clientEntry}"></script>
|
210
|
+
</body>
|
211
|
+
</html>
|
212
|
+
`);
|
213
|
+
}
|
214
|
+
});
|
215
|
+
|
216
|
+
app.listen(port, () => {
|
217
|
+
console.log(`Server running at http://localhost:${port}`);
|
218
|
+
});
|