frontend-hamroun 1.2.80 → 1.2.83
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/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.client.cjs +1 -1
- package/dist/index.client.js +2 -2
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/{renderer-Din1y3YM.cjs → renderer-BL3gq8cW.cjs} +2 -2
- package/dist/{renderer-Din1y3YM.cjs.map → renderer-BL3gq8cW.cjs.map} +1 -1
- package/dist/{renderer-Bo9zkUZ_.js → renderer-Dyy-o05F.js} +2 -2
- package/dist/{renderer-Bo9zkUZ_.js.map → renderer-Dyy-o05F.js.map} +1 -1
- package/dist/{server-renderer-QHt45Ip2.js → server-renderer-C1WXH-zV.js} +99 -73
- package/dist/server-renderer-C1WXH-zV.js.map +1 -0
- package/dist/server-renderer-Chs-nmJm.cjs +2 -0
- package/dist/server-renderer-Chs-nmJm.cjs.map +1 -0
- package/dist/server-renderer.cjs +1 -1
- package/dist/server-renderer.js +1 -1
- package/package.json +1 -1
- package/templates/basic-app/src/App.jsx +16 -0
- package/templates/basic-app/src/client.jsx +5 -0
- package/templates/basic-app/src/components/Counter.jsx +13 -0
- package/templates/basic-app/src/jsx-shim.js +3 -0
- package/templates/basic-app/src/jsx-shim.ts +7 -0
- package/templates/basic-app/src/main.jsx +98 -0
- package/templates/basic-app/src/server.js +47 -0
- package/templates/complete-app/api/hello.js +0 -0
- package/templates/complete-app/lib/frontend-hamroun.js +182 -0
- package/templates/complete-app/package.json +18 -0
- package/templates/complete-app/pages/about.js +119 -0
- package/templates/complete-app/pages/about.jsx +0 -0
- package/templates/complete-app/pages/index.js +157 -0
- package/templates/complete-app/pages/index.jsx +0 -0
- package/templates/complete-app/pages/wasm-demo.js +290 -0
- package/templates/complete-app/pages/wasm-demo.jsx +0 -0
- package/templates/complete-app/public/client.js +89 -0
- package/templates/complete-app/public/index.html +118 -0
- package/templates/complete-app/public/styles.css +76 -0
- package/templates/complete-app/server.js +226 -0
- package/templates/complete-app/src/App.tsx +59 -0
- package/templates/complete-app/src/client.tsx +18 -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/{ssr-template → complete-app}/vite.config.js +16 -5
- package/templates/complete-app/vite.config.ts +30 -0
- package/templates/complete-app/wasm/build.bat +0 -0
- package/templates/complete-app/wasm/build.sh +0 -0
- package/templates/complete-app/wasm/example.go +0 -0
- package/templates/fullstack-app/build/main.css +874 -874
- package/templates/fullstack-app/build/main.css.map +7 -7
- package/templates/fullstack-app/build/main.js +996 -967
- package/templates/fullstack-app/build/main.js.map +7 -7
- package/templates/fullstack-app/package-lock.json +6301 -0
- package/templates/fullstack-app/public/styles.css +768 -768
- package/templates/go/example.go +154 -99
- package/templates/ssr-template/dist/client/assets/main-D-VH3xOb.js +1 -0
- package/templates/ssr-template/dist/client/index.html +23 -0
- package/templates/ssr-template/dist/client.js +951 -0
- package/templates/ssr-template/dist/server.js +739 -0
- package/templates/ssr-template/esbuild.config.js +33 -0
- package/templates/ssr-template/jsx-shim.js +1 -0
- package/templates/ssr-template/package.json +14 -8
- package/templates/ssr-template/src/App.tsx +847 -49
- package/templates/ssr-template/src/client.tsx +3 -17
- package/templates/ssr-template/src/server.ts +21 -204
- package/templates/ssr-template/tsconfig.json +9 -8
- package/templates/ssr-template/tsconfig.server.json +6 -14
- package/templates/ssr-template/vite.config.ts +19 -17
- package/templates/wasm/build-wasm.js +228 -0
- package/templates/wasm/dist/assets/index-BNqTDBdE.js +295 -0
- package/templates/wasm/dist/example.wasm +0 -0
- package/templates/wasm/dist/index.html +53 -0
- package/templates/{go-wasm-app/public/wasm → wasm/dist}/wasm_exec.js +572 -561
- package/templates/wasm/esbuild.config.js +63 -0
- package/templates/wasm/go/main.go +256 -0
- package/templates/wasm/go/wasm_exec.js +0 -0
- package/templates/wasm/index.html +97 -0
- package/templates/wasm/jsx-shim.js +9 -0
- package/templates/wasm/package-lock.json +4577 -0
- package/templates/wasm/package.json +25 -0
- package/templates/wasm/public/example.wasm +0 -0
- package/templates/wasm/public/wasm_exec.js +572 -0
- package/templates/wasm/src/App.tsx +550 -0
- package/templates/wasm/src/client.tsx +220 -0
- package/templates/wasm/src/index.tsx +21 -0
- package/templates/wasm/src/server.ts +145 -0
- package/templates/wasm/tsconfig.json +21 -0
- package/templates/wasm/tsconfig.node.json +13 -0
- package/templates/wasm/tsconfig.server.json +23 -0
- package/templates/wasm/vite.config.ts +38 -0
- package/templates/wasm/wasm-loader.js +103 -0
- package/dist/server-renderer-CqIpQ-od.cjs +0 -2
- package/dist/server-renderer-CqIpQ-od.cjs.map +0 -1
- package/dist/server-renderer-QHt45Ip2.js.map +0 -1
- package/templates/basic-app/bun.lock +0 -196
- package/templates/basic-app/docs/rapport_pfe.aux +0 -27
- package/templates/basic-app/docs/rapport_pfe.out +0 -10
- package/templates/basic-app/docs/rapport_pfe.pdf +0 -0
- package/templates/basic-app/docs/rapport_pfe.tex +0 -68
- package/templates/basic-app/docs/rapport_pfe.toc +0 -14
- package/templates/basic-app/package-lock.json +0 -4185
- package/templates/go-wasm-app/README.md +0 -38
- package/templates/go-wasm-app/babel.config.js +0 -15
- package/templates/go-wasm-app/build-client.js +0 -49
- package/templates/go-wasm-app/build-wasm.js +0 -237
- package/templates/go-wasm-app/package.json +0 -23
- package/templates/go-wasm-app/public/index.html +0 -128
- package/templates/go-wasm-app/public/styles.css +0 -197
- package/templates/go-wasm-app/public/wasm/example.wasm +0 -0
- package/templates/go-wasm-app/public/wasm/wasm_exec_node.js +0 -39
- package/templates/go-wasm-app/server.js +0 -521
- package/templates/go-wasm-app/src/App.jsx +0 -38
- package/templates/go-wasm-app/src/app.js +0 -153
- package/templates/go-wasm-app/src/client.js +0 -57
- package/templates/go-wasm-app/src/components/Footer.jsx +0 -13
- package/templates/go-wasm-app/src/components/Header.jsx +0 -19
- package/templates/go-wasm-app/src/components/WasmDemo.jsx +0 -120
- package/templates/go-wasm-app/src/main.jsx +0 -12
- package/templates/go-wasm-app/src/wasm/example.go +0 -75
- package/templates/go-wasm-app/tsconfig.server.json +0 -18
- package/templates/go-wasm-app/vite.config.js +0 -34
- package/templates/ssr-template/package-lock.json +0 -2478
- package/templates/ssr-template/public/index.html +0 -47
- package/templates/ssr-template/server.js +0 -369
- /package/templates/{ssr-template → complete-app}/client.js +0 -0
- /package/templates/{ssr-template → complete-app}/readme.md +0 -0
- /package/templates/{ssr-template → complete-app}/server.ts +0 -0
- /package/templates/{ssr-template → complete-app}/src/client.ts +0 -0
- /package/templates/{ssr-template → complete-app}/src/pages/index.tsx +0 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
/* Color scheme */
|
2
|
+
:root {
|
3
|
+
--primary-color: #0066cc;
|
4
|
+
--primary-hover: #004c99;
|
5
|
+
--secondary-color: #6c757d;
|
6
|
+
--light-bg: #f8f9fa;
|
7
|
+
--light-border: #dee2e6;
|
8
|
+
--dark-text: #212529;
|
9
|
+
}
|
10
|
+
|
11
|
+
body {
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
13
|
+
background-color: var(--light-bg);
|
14
|
+
color: var(--dark-text);
|
15
|
+
line-height: 1.5;
|
16
|
+
margin: 0;
|
17
|
+
padding: 0;
|
18
|
+
}
|
19
|
+
|
20
|
+
.container {
|
21
|
+
max-width: 1140px;
|
22
|
+
margin: 0 auto;
|
23
|
+
padding: 2rem 1rem;
|
24
|
+
}
|
25
|
+
|
26
|
+
.logo {
|
27
|
+
font-size: 2rem;
|
28
|
+
font-weight: bold;
|
29
|
+
color: var(--primary-color);
|
30
|
+
margin-bottom: 1.5rem;
|
31
|
+
}
|
32
|
+
|
33
|
+
h1, h2, h3 {
|
34
|
+
margin-top: 0;
|
35
|
+
margin-bottom: 1rem;
|
36
|
+
}
|
37
|
+
|
38
|
+
ul {
|
39
|
+
padding-left: 1.5rem;
|
40
|
+
}
|
41
|
+
|
42
|
+
li {
|
43
|
+
margin-bottom: 0.5rem;
|
44
|
+
}
|
45
|
+
|
46
|
+
.button, button {
|
47
|
+
display: inline-block;
|
48
|
+
font-weight: 400;
|
49
|
+
color: #fff;
|
50
|
+
text-align: center;
|
51
|
+
vertical-align: middle;
|
52
|
+
background-color: var(--primary-color);
|
53
|
+
border: 1px solid var(--primary-color);
|
54
|
+
padding: 0.375rem 0.75rem;
|
55
|
+
font-size: 1rem;
|
56
|
+
line-height: 1.5;
|
57
|
+
border-radius: 0.25rem;
|
58
|
+
cursor: pointer;
|
59
|
+
text-decoration: none;
|
60
|
+
transition: background-color 0.15s ease-in-out;
|
61
|
+
}
|
62
|
+
|
63
|
+
.button:hover, button:hover {
|
64
|
+
background-color: var(--primary-hover);
|
65
|
+
border-color: var(--primary-hover);
|
66
|
+
text-decoration: none;
|
67
|
+
}
|
68
|
+
|
69
|
+
.footer {
|
70
|
+
padding: 1.5rem 0;
|
71
|
+
margin-top: 3rem;
|
72
|
+
background-color: #f1f1f1;
|
73
|
+
color: var(--secondary-color);
|
74
|
+
text-align: center;
|
75
|
+
font-size: 0.875rem;
|
76
|
+
}
|
@@ -0,0 +1,226 @@
|
|
1
|
+
import express from 'express';
|
2
|
+
import path from 'path';
|
3
|
+
import { fileURLToPath } from 'url';
|
4
|
+
import fs from 'fs';
|
5
|
+
// Fix imports by using only the exports that exist
|
6
|
+
import {
|
7
|
+
renderToString,
|
8
|
+
jsx,
|
9
|
+
requestLogger,
|
10
|
+
errorHandler,
|
11
|
+
notFoundHandler,
|
12
|
+
rateLimit
|
13
|
+
} from './lib/frontend-hamroun.js';
|
14
|
+
import dotenv from 'dotenv';
|
15
|
+
import compression from 'compression';
|
16
|
+
import cors from 'cors';
|
17
|
+
|
18
|
+
// Load environment variables
|
19
|
+
dotenv.config();
|
20
|
+
|
21
|
+
// Get __dirname equivalent in ESM
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
23
|
+
const __dirname = path.dirname(__filename);
|
24
|
+
|
25
|
+
// Create Express app
|
26
|
+
const app = express();
|
27
|
+
const PORT = process.env.PORT || 3000;
|
28
|
+
|
29
|
+
// Middleware
|
30
|
+
app.use(compression()); // Compress responses
|
31
|
+
app.use(express.json()); // Parse JSON requests
|
32
|
+
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded requests
|
33
|
+
app.use(cors()); // Enable CORS
|
34
|
+
app.use(requestLogger); // Log all requests
|
35
|
+
|
36
|
+
// Add rate limiting to prevent abuse
|
37
|
+
app.use(rateLimit({
|
38
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
39
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
40
|
+
}));
|
41
|
+
|
42
|
+
// Serve static files from the public directory
|
43
|
+
// Important: This middleware should be defined BEFORE your catch-all route
|
44
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
45
|
+
|
46
|
+
// Add correct MIME type for client.js module
|
47
|
+
app.get('/client.js', (req, res) => {
|
48
|
+
res.type('application/javascript').sendFile(path.join(__dirname, 'public', 'client.js'));
|
49
|
+
});
|
50
|
+
|
51
|
+
// Serve WebAssembly files with correct MIME type
|
52
|
+
app.get('*.wasm', (req, res, next) => {
|
53
|
+
res.type('application/wasm');
|
54
|
+
next();
|
55
|
+
});
|
56
|
+
|
57
|
+
// Add API routes example
|
58
|
+
app.get('/api/hello', (req, res) => {
|
59
|
+
res.json({
|
60
|
+
message: 'Hello from the API!',
|
61
|
+
serverTime: new Date().toISOString()
|
62
|
+
});
|
63
|
+
});
|
64
|
+
|
65
|
+
// Find the pages directory
|
66
|
+
const getPagesDirectory = () => {
|
67
|
+
return path.join(__dirname, 'pages');
|
68
|
+
};
|
69
|
+
|
70
|
+
// Helper to check if a file exists
|
71
|
+
const fileExists = async (filePath) => {
|
72
|
+
try {
|
73
|
+
await fs.promises.access(filePath);
|
74
|
+
return true;
|
75
|
+
} catch {
|
76
|
+
return false;
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
80
|
+
// Map URL path to component path
|
81
|
+
const getComponentPath = async (urlPath) => {
|
82
|
+
const pagesDir = getPagesDirectory();
|
83
|
+
|
84
|
+
// Handle root path
|
85
|
+
if (urlPath === '/') {
|
86
|
+
const indexPath = path.join(pagesDir, 'index.js');
|
87
|
+
if (await fileExists(indexPath)) {
|
88
|
+
return {
|
89
|
+
componentPath: indexPath,
|
90
|
+
params: {}
|
91
|
+
};
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
// Try direct match (e.g., /about -> /pages/about.js)
|
96
|
+
const possibleExtensions = ['.js'];
|
97
|
+
|
98
|
+
for (const ext of possibleExtensions) {
|
99
|
+
const directPath = path.join(pagesDir, `${urlPath.slice(1)}${ext}`);
|
100
|
+
if (await fileExists(directPath)) {
|
101
|
+
return {
|
102
|
+
componentPath: directPath,
|
103
|
+
params: {}
|
104
|
+
};
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
// Try directory index (e.g., /about -> /pages/about/index.js)
|
109
|
+
for (const ext of possibleExtensions) {
|
110
|
+
const dirIndexPath = path.join(pagesDir, urlPath.slice(1), `index${ext}`);
|
111
|
+
if (await fileExists(dirIndexPath)) {
|
112
|
+
return {
|
113
|
+
componentPath: dirIndexPath,
|
114
|
+
params: {}
|
115
|
+
};
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
// No match found
|
120
|
+
return null;
|
121
|
+
};
|
122
|
+
|
123
|
+
// Handle all GET routes with SSR - explicitly exclude /public/ paths
|
124
|
+
app.get('*', async (req, res, next) => {
|
125
|
+
try {
|
126
|
+
// Skip API routes
|
127
|
+
if (req.path.startsWith('/api/')) {
|
128
|
+
return next();
|
129
|
+
}
|
130
|
+
|
131
|
+
// Skip static assets - check file extensions that should be handled as static
|
132
|
+
if (req.path.match(/\.(js|css|ico|png|jpg|jpeg|gif|svg|wasm|txt|pdf|json|map)$/)) {
|
133
|
+
return next();
|
134
|
+
}
|
135
|
+
|
136
|
+
// Try to find the matching component
|
137
|
+
const routeResult = await getComponentPath(req.path);
|
138
|
+
|
139
|
+
if (!routeResult) {
|
140
|
+
return res.status(404).send(`
|
141
|
+
<!DOCTYPE html>
|
142
|
+
<html>
|
143
|
+
<head>
|
144
|
+
<title>404 - Page Not Found</title>
|
145
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
146
|
+
<link href="/styles.css" rel="stylesheet" type="text/css">
|
147
|
+
</head>
|
148
|
+
<body>
|
149
|
+
<div id="app">
|
150
|
+
<div class="container">
|
151
|
+
<h1>404 - Page Not Found</h1>
|
152
|
+
<p>The page you requested could not be found.</p>
|
153
|
+
<a href="/" class="button">Go to Home</a>
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
<script id="__APP_DATA__" type="application/json">${JSON.stringify({
|
157
|
+
path: req.path,
|
158
|
+
error: 'not_found'
|
159
|
+
})}</script>
|
160
|
+
<script type="module" src="/client.js"></script>
|
161
|
+
</body>
|
162
|
+
</html>
|
163
|
+
`);
|
164
|
+
}
|
165
|
+
|
166
|
+
// Import the component
|
167
|
+
try {
|
168
|
+
const componentUrl = `file://${routeResult.componentPath}`;
|
169
|
+
const moduleImport = await import(componentUrl);
|
170
|
+
const PageComponent = moduleImport.default || moduleImport;
|
171
|
+
|
172
|
+
if (!PageComponent || typeof PageComponent !== 'function') {
|
173
|
+
throw new Error(`Invalid component in ${routeResult.componentPath}`);
|
174
|
+
}
|
175
|
+
|
176
|
+
// Create props with route data
|
177
|
+
const initialProps = {
|
178
|
+
params: routeResult.params,
|
179
|
+
path: req.path,
|
180
|
+
query: req.query,
|
181
|
+
api: {
|
182
|
+
serverTime: new Date().toISOString()
|
183
|
+
}
|
184
|
+
};
|
185
|
+
|
186
|
+
// Render the component
|
187
|
+
const result = PageComponent(initialProps);
|
188
|
+
let html = renderToString(result);
|
189
|
+
|
190
|
+
// Send complete HTML with hydration data
|
191
|
+
res.send(`
|
192
|
+
<!DOCTYPE html>
|
193
|
+
<html lang="en">
|
194
|
+
<head>
|
195
|
+
<meta charset="UTF-8">
|
196
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
197
|
+
<title>Frontend Hamroun App</title>
|
198
|
+
<link href="/styles.css" rel="stylesheet" type="text/css">
|
199
|
+
</head>
|
200
|
+
<body>
|
201
|
+
<div id="app">${html}</div>
|
202
|
+
<script id="__APP_DATA__" type="application/json">${JSON.stringify(initialProps)}</script>
|
203
|
+
<script type="module" src="/client.js"></script>
|
204
|
+
</body>
|
205
|
+
</html>
|
206
|
+
`);
|
207
|
+
} catch (error) {
|
208
|
+
console.error('Error rendering component:', error);
|
209
|
+
next(error);
|
210
|
+
}
|
211
|
+
} catch (error) {
|
212
|
+
next(error);
|
213
|
+
}
|
214
|
+
});
|
215
|
+
|
216
|
+
// Add error handler middleware
|
217
|
+
app.use(errorHandler);
|
218
|
+
|
219
|
+
// Add 404 handler for API routes
|
220
|
+
app.use(notFoundHandler);
|
221
|
+
|
222
|
+
// Start the server
|
223
|
+
app.listen(PORT, () => {
|
224
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
225
|
+
console.log(`Mode: ${process.env.NODE_ENV || 'development'}`);
|
226
|
+
});
|
@@ -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,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,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
|
+
});
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ESNext",
|
4
|
+
"useDefineForClassFields": true,
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
6
|
+
"allowJs": false,
|
7
|
+
"skipLibCheck": true,
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"allowSyntheticDefaultImports": true,
|
10
|
+
"strict": true,
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
12
|
+
"module": "ESNext",
|
13
|
+
"moduleResolution": "Node",
|
14
|
+
"resolveJsonModule": true,
|
15
|
+
"isolatedModules": true,
|
16
|
+
"noEmit": true,
|
17
|
+
"jsx": "preserve",
|
18
|
+
"jsxFactory": "createElement",
|
19
|
+
"jsxFragmentFactory": "Fragment"
|
20
|
+
},
|
21
|
+
"include": ["src"]
|
22
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"extends": "./tsconfig.json",
|
3
|
+
"compilerOptions": {
|
4
|
+
"target": "ES2020",
|
5
|
+
"module": "NodeNext",
|
6
|
+
"moduleResolution": "NodeNext",
|
7
|
+
"esModuleInterop": true,
|
8
|
+
"outDir": "./dist/server",
|
9
|
+
"declaration": true,
|
10
|
+
"sourceMap": true,
|
11
|
+
"strict": true,
|
12
|
+
"skipLibCheck": true,
|
13
|
+
"jsx": "react",
|
14
|
+
"jsxFactory": "createElement",
|
15
|
+
"jsxFragmentFactory": "Fragment"
|
16
|
+
},
|
17
|
+
"include": ["server.ts", "src/pages/**/*.tsx"],
|
18
|
+
"exclude": ["node_modules", "dist"]
|
19
|
+
}
|
@@ -17,11 +17,22 @@ export default defineConfig({
|
|
17
17
|
input: {
|
18
18
|
client: resolve(__dirname, 'src/client.ts')
|
19
19
|
},
|
20
|
-
output:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
output: [
|
21
|
+
{
|
22
|
+
// ESM output
|
23
|
+
entryFileNames: 'assets/[name].mjs',
|
24
|
+
chunkFileNames: 'assets/[name]-[hash].mjs',
|
25
|
+
assetFileNames: 'assets/[name]-[hash].[ext]',
|
26
|
+
format: 'es',
|
27
|
+
},
|
28
|
+
{
|
29
|
+
// CommonJS output
|
30
|
+
entryFileNames: 'assets/[name].js',
|
31
|
+
chunkFileNames: 'assets/[name]-[hash].js',
|
32
|
+
assetFileNames: 'assets/[name]-[hash].[ext]',
|
33
|
+
format: 'cjs',
|
34
|
+
}
|
35
|
+
]
|
25
36
|
}
|
26
37
|
},
|
27
38
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { defineConfig } from 'vite';
|
2
|
+
import path from 'path';
|
3
|
+
|
4
|
+
export default defineConfig({
|
5
|
+
resolve: {
|
6
|
+
alias: {
|
7
|
+
'frontend-hamroun': path.resolve(__dirname, 'node_modules/frontend-hamroun')
|
8
|
+
}
|
9
|
+
},
|
10
|
+
build: {
|
11
|
+
outDir: 'dist',
|
12
|
+
ssr: 'src/server.ts',
|
13
|
+
rollupOptions: {
|
14
|
+
input: {
|
15
|
+
client: './src/client.tsx',
|
16
|
+
server: './src/server.ts'
|
17
|
+
},
|
18
|
+
output: {
|
19
|
+
entryFileNames: '[name].js',
|
20
|
+
chunkFileNames: 'assets/[name]-[hash].js',
|
21
|
+
assetFileNames: 'assets/[name]-[hash].[ext]'
|
22
|
+
}
|
23
|
+
}
|
24
|
+
},
|
25
|
+
esbuild: {
|
26
|
+
jsxFactory: '_jsx',
|
27
|
+
jsxFragment: '_Fragment',
|
28
|
+
jsxInject: `import { jsx as _jsx, Fragment as _Fragment } from 'frontend-hamroun'`
|
29
|
+
}
|
30
|
+
});
|