frontend-hamroun 1.2.26 → 1.2.28
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 +7 -0
- package/dist/index.client.d.ts +1 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +26 -1
- package/templates/fullstack-app/README.md +37 -0
- package/templates/fullstack-app/build/main.css +664 -0
- package/templates/fullstack-app/build/main.css.map +7 -0
- package/templates/fullstack-app/build/main.js +682 -0
- package/templates/fullstack-app/build/main.js.map +7 -0
- package/templates/fullstack-app/build.ts +211 -0
- package/templates/fullstack-app/index.html +26 -3
- package/templates/fullstack-app/package-lock.json +2402 -438
- package/templates/fullstack-app/package.json +24 -9
- package/templates/fullstack-app/postcss.config.js +6 -0
- package/templates/fullstack-app/process-tailwind.js +45 -0
- package/templates/fullstack-app/public/_redirects +1 -0
- package/templates/fullstack-app/public/route-handler.js +47 -0
- package/templates/fullstack-app/public/spa-fix.html +17 -0
- package/templates/fullstack-app/public/styles.css +768 -0
- package/templates/fullstack-app/public/tailwind.css +15 -0
- package/templates/fullstack-app/server.js +101 -44
- package/templates/fullstack-app/server.ts +402 -39
- package/templates/fullstack-app/src/README.md +55 -0
- package/templates/fullstack-app/src/client.js +83 -16
- package/templates/fullstack-app/src/components/Layout.tsx +45 -0
- package/templates/fullstack-app/src/components/UserList.tsx +27 -0
- package/templates/fullstack-app/src/config.ts +42 -0
- package/templates/fullstack-app/src/data/api.ts +71 -0
- package/templates/fullstack-app/src/main.tsx +219 -7
- package/templates/fullstack-app/src/pages/about/index.tsx +67 -0
- package/templates/fullstack-app/src/pages/index.tsx +30 -60
- package/templates/fullstack-app/src/pages/users.tsx +60 -0
- package/templates/fullstack-app/src/router.ts +255 -0
- package/templates/fullstack-app/src/styles.css +5 -0
- package/templates/fullstack-app/tailwind.config.js +11 -0
- package/templates/fullstack-app/tsconfig.json +18 -0
- package/templates/fullstack-app/vite.config.js +53 -6
- package/templates/ssr-template/readme.md +50 -0
- package/templates/ssr-template/src/client.ts +46 -14
- package/templates/ssr-template/src/server.ts +190 -18
@@ -0,0 +1,15 @@
|
|
1
|
+
/* All Tailwind directives */
|
2
|
+
@tailwind base;
|
3
|
+
@tailwind components;
|
4
|
+
@tailwind utilities;
|
5
|
+
|
6
|
+
/* Custom styles */
|
7
|
+
body {
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
9
|
+
line-height: 1.6;
|
10
|
+
}
|
11
|
+
|
12
|
+
/* Any custom utility classes can go here */
|
13
|
+
.custom-container {
|
14
|
+
@apply max-w-4xl mx-auto px-4 py-8;
|
15
|
+
}
|
@@ -65,7 +65,7 @@ const store = {
|
|
65
65
|
]
|
66
66
|
};
|
67
67
|
|
68
|
-
// API
|
68
|
+
// Core API routes (available without file-based routing)
|
69
69
|
app.get('/api/hello', (req, res) => {
|
70
70
|
res.json({
|
71
71
|
message: "Hello from the API!",
|
@@ -82,61 +82,87 @@ app.get('/api/hello', (req, res) => {
|
|
82
82
|
});
|
83
83
|
});
|
84
84
|
|
85
|
-
//
|
85
|
+
// REST API endpoints for store data
|
86
|
+
app.get('/api/users', (req, res) => {
|
87
|
+
res.json(store.users);
|
88
|
+
});
|
89
|
+
|
90
|
+
app.get('/api/users/:id', (req, res) => {
|
91
|
+
const user = store.users.find(u => u.id === parseInt(req.params.id));
|
92
|
+
if (user) {
|
93
|
+
res.json(user);
|
94
|
+
} else {
|
95
|
+
res.status(404).json({ error: 'User not found' });
|
96
|
+
}
|
97
|
+
});
|
98
|
+
|
99
|
+
app.get('/api/posts', (req, res) => {
|
100
|
+
const { authorId } = req.query;
|
101
|
+
|
102
|
+
if (authorId) {
|
103
|
+
const filteredPosts = store.posts.filter(p => p.authorId === parseInt(authorId));
|
104
|
+
return res.json(filteredPosts);
|
105
|
+
}
|
106
|
+
|
107
|
+
res.json(store.posts);
|
108
|
+
});
|
109
|
+
|
110
|
+
// Improved dynamic API route loading with better error handling
|
86
111
|
const apiDir = path.join(__dirname, 'api');
|
87
112
|
if (fs.existsSync(apiDir)) {
|
88
113
|
console.log('Loading API routes from directory...');
|
89
114
|
|
90
115
|
// Function to map file paths to API routes
|
91
116
|
const registerApiRoutes = async (dir, routePrefix = '') => {
|
92
|
-
|
93
|
-
|
94
|
-
for (const entry of entries) {
|
95
|
-
const fullPath = path.join(dir, entry.name);
|
117
|
+
try {
|
118
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
96
119
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
120
|
+
for (const entry of entries) {
|
121
|
+
const fullPath = path.join(dir, entry.name);
|
122
|
+
|
123
|
+
if (entry.isDirectory()) {
|
124
|
+
// Process directories recursively
|
125
|
+
await registerApiRoutes(fullPath, `${routePrefix}/${entry.name}`);
|
126
|
+
} else if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
127
|
+
// Process API file
|
128
|
+
try {
|
129
|
+
const fileName = entry.name.replace(/\.[^/.]+$/, ""); // Remove extension
|
130
|
+
const routePath = fileName === 'index'
|
131
|
+
? routePrefix
|
132
|
+
: fileName.startsWith('[') && fileName.endsWith(']')
|
133
|
+
? `${routePrefix}/:${fileName.slice(1, -1)}` // Convert [param] to :param
|
134
|
+
: `${routePrefix}/${fileName}`;
|
135
|
+
|
136
|
+
// Import the route module
|
137
|
+
const module = await import(`file://${fullPath}`);
|
138
|
+
|
139
|
+
// Register handlers for HTTP methods
|
140
|
+
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
|
141
|
+
if (typeof module[method] === 'function') {
|
142
|
+
console.log(` Registered ${method.toUpperCase()} ${routePath}`);
|
143
|
+
app[method](`/api${routePath}`, module[method]);
|
144
|
+
}
|
145
|
+
});
|
146
|
+
|
147
|
+
// Special case for 'delete' which might be named 'delete_' in some files
|
148
|
+
if (typeof module['delete_'] === 'function') {
|
149
|
+
console.log(` Registered DELETE ${routePath}`);
|
150
|
+
app.delete(`/api${routePath}`, module['delete_']);
|
118
151
|
}
|
119
|
-
})
|
120
|
-
|
121
|
-
// Special case for 'delete' which might be named 'delete_' in some files
|
122
|
-
if (typeof module['delete_'] === 'function') {
|
123
|
-
console.log(` Registered DELETE ${routePath}`);
|
124
|
-
app.delete(`/api${routePath}`, module['delete_']);
|
152
|
+
} catch (err) {
|
153
|
+
console.error(`Error loading API route ${fullPath}:`, err);
|
125
154
|
}
|
126
|
-
} catch (err) {
|
127
|
-
console.error(`Error loading API route ${fullPath}:`, err);
|
128
155
|
}
|
129
156
|
}
|
157
|
+
} catch (err) {
|
158
|
+
console.error(`Error reading directory ${dir}:`, err);
|
130
159
|
}
|
131
160
|
};
|
132
161
|
|
133
162
|
// Start registering API routes
|
134
|
-
|
135
|
-
await registerApiRoutes(apiDir);
|
136
|
-
console.log('API routes loaded successfully');
|
137
|
-
} catch (err) {
|
163
|
+
registerApiRoutes(apiDir).catch(err => {
|
138
164
|
console.error('Error loading API routes:', err);
|
139
|
-
}
|
165
|
+
});
|
140
166
|
} else {
|
141
167
|
console.log('API directory not found, skipping API route loading');
|
142
168
|
}
|
@@ -168,7 +194,7 @@ io.on('connection', (socket) => {
|
|
168
194
|
});
|
169
195
|
});
|
170
196
|
|
171
|
-
// In production or if using SSR, handle all
|
197
|
+
// In production or if using SSR, handle all routes
|
172
198
|
app.get('*', (req, res, next) => {
|
173
199
|
// Skip API routes
|
174
200
|
if (req.path.startsWith('/api/')) {
|
@@ -179,13 +205,44 @@ app.get('*', (req, res, next) => {
|
|
179
205
|
if (process.env.NODE_ENV === 'production') {
|
180
206
|
const indexPath = path.join(__dirname, 'dist', 'index.html');
|
181
207
|
if (fs.existsSync(indexPath)) {
|
182
|
-
|
208
|
+
try {
|
209
|
+
// Read the index.html file
|
210
|
+
let html = fs.readFileSync(indexPath, 'utf8');
|
211
|
+
|
212
|
+
// Create initial state for client hydration
|
213
|
+
const initialState = {
|
214
|
+
route: req.path,
|
215
|
+
timestamp: new Date().toISOString()
|
216
|
+
};
|
217
|
+
|
218
|
+
// Inject initial state for hydration
|
219
|
+
html = html.replace(
|
220
|
+
'</head>',
|
221
|
+
`<script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script></head>`
|
222
|
+
);
|
223
|
+
|
224
|
+
res.send(html);
|
225
|
+
} catch (error) {
|
226
|
+
console.error('Error serving HTML:', error);
|
227
|
+
res.status(500).send('Server Error');
|
228
|
+
}
|
183
229
|
} else {
|
184
230
|
res.status(404).send('Not found');
|
185
231
|
}
|
186
232
|
} else {
|
187
|
-
// In development,
|
188
|
-
|
233
|
+
// In development, use Vite's dev server
|
234
|
+
// Instead of redirecting, proxy the request to maintain the URL path
|
235
|
+
// This allows Vite to handle the route properly
|
236
|
+
const viteDevServerUrl = 'http://localhost:5173';
|
237
|
+
|
238
|
+
// Tell the client which route to handle via response headers
|
239
|
+
res.setHeader('x-original-route', req.path);
|
240
|
+
|
241
|
+
// Forward the request to the Vite dev server with the original path
|
242
|
+
res.redirect(302, `${viteDevServerUrl}${req.path}`);
|
243
|
+
|
244
|
+
// Note: If the above still causes 404s, you can try this alternative
|
245
|
+
// res.redirect(302, `${viteDevServerUrl}/?_path=${encodeURIComponent(req.path)}`);
|
189
246
|
}
|
190
247
|
});
|
191
248
|
|
@@ -1,77 +1,440 @@
|
|
1
|
-
import express from 'express';
|
1
|
+
import express, { Request, Response, NextFunction } from 'express';
|
2
2
|
import path from 'path';
|
3
3
|
import { fileURLToPath } from 'url';
|
4
|
-
import fs from 'fs';
|
4
|
+
import fs from 'fs/promises';
|
5
|
+
import { existsSync } from 'fs';
|
6
|
+
import dotenv from 'dotenv';
|
7
|
+
import { createServer } from 'http';
|
8
|
+
import { Server as SocketServer } from 'socket.io';
|
9
|
+
import compression from 'compression';
|
10
|
+
import cors from 'cors';
|
11
|
+
import * as esbuild from 'esbuild';
|
5
12
|
|
6
|
-
//
|
13
|
+
// Setup environment
|
14
|
+
dotenv.config();
|
7
15
|
const __filename = fileURLToPath(import.meta.url);
|
8
16
|
const __dirname = path.dirname(__filename);
|
9
17
|
|
10
18
|
// Create Express app
|
11
19
|
const app = express();
|
12
20
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
21
|
+
const httpServer = createServer(app);
|
13
22
|
|
14
|
-
//
|
15
|
-
|
23
|
+
// Set development mode
|
24
|
+
if (process.env.NODE_ENV === undefined) {
|
25
|
+
process.env.NODE_ENV = 'development';
|
26
|
+
}
|
27
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
16
28
|
|
17
|
-
//
|
29
|
+
// Create socket.io server
|
30
|
+
const io = new SocketServer(httpServer, {
|
31
|
+
cors: {
|
32
|
+
origin: isDev ? '*' : false,
|
33
|
+
methods: ['GET', 'POST']
|
34
|
+
}
|
35
|
+
});
|
36
|
+
|
37
|
+
// Middleware setup
|
18
38
|
app.use(express.json());
|
19
39
|
app.use(express.urlencoded({ extended: true }));
|
40
|
+
app.use(compression());
|
41
|
+
app.use(cors());
|
42
|
+
if (isDev) {
|
43
|
+
app.use((req, res, next) => {
|
44
|
+
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
45
|
+
next();
|
46
|
+
});
|
47
|
+
}
|
48
|
+
|
49
|
+
// Sample data store
|
50
|
+
const store = {
|
51
|
+
users: [
|
52
|
+
{ id: 1, name: 'User 1', email: 'user1@example.com' },
|
53
|
+
{ id: 2, name: 'User 2', email: 'user2@example.com' },
|
54
|
+
{ id: 3, name: 'User 3', email: 'user3@example.com' }
|
55
|
+
],
|
56
|
+
posts: [
|
57
|
+
{ id: 1, title: 'Post 1', content: 'Content for post 1', authorId: 1 },
|
58
|
+
{ id: 2, title: 'Post 2', content: 'Content for post 2', authorId: 2 },
|
59
|
+
{ id: 3, title: 'Post 3', content: 'Content for post 3', authorId: 1 }
|
60
|
+
]
|
61
|
+
};
|
20
62
|
|
21
|
-
//
|
63
|
+
// API Routes
|
22
64
|
app.get('/api/hello', (req, res) => {
|
23
65
|
res.json({
|
24
66
|
message: 'Hello from the API!',
|
25
|
-
time: new Date().toISOString()
|
26
|
-
environment: process.env.NODE_ENV || 'development'
|
67
|
+
time: new Date().toISOString()
|
27
68
|
});
|
28
69
|
});
|
29
70
|
|
30
|
-
|
31
|
-
|
71
|
+
app.get('/api/users', (req, res) => {
|
72
|
+
res.json(store.users);
|
73
|
+
});
|
74
|
+
|
75
|
+
app.get('/api/users/:id', (req, res) => {
|
76
|
+
const user = store.users.find(u => u.id === parseInt(req.params.id));
|
77
|
+
if (user) {
|
78
|
+
res.json(user);
|
79
|
+
} else {
|
80
|
+
res.status(404).json({ error: 'User not found' });
|
81
|
+
}
|
82
|
+
});
|
83
|
+
|
84
|
+
// Setup esbuild
|
85
|
+
let esbuildContext;
|
86
|
+
|
87
|
+
async function setupEsbuild() {
|
88
|
+
const buildDir = path.join(__dirname, 'build');
|
89
|
+
if (!existsSync(buildDir)) {
|
90
|
+
await fs.mkdir(buildDir, { recursive: true });
|
91
|
+
}
|
92
|
+
|
93
|
+
// Import PostCSS, Tailwind and Autoprefixer when in development mode
|
94
|
+
let postcssPlugin = null;
|
95
|
+
if (isDev) {
|
96
|
+
try {
|
97
|
+
const postcss = (await import('postcss')).default;
|
98
|
+
const tailwindcss = (await import('tailwindcss')).default;
|
99
|
+
const autoprefixer = (await import('autoprefixer')).default;
|
100
|
+
|
101
|
+
// Create a plugin to process CSS with Tailwind
|
102
|
+
postcssPlugin = {
|
103
|
+
name: 'postcss-tailwind',
|
104
|
+
setup(build) {
|
105
|
+
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
106
|
+
const css = await fs.readFile(args.path, 'utf8');
|
107
|
+
try {
|
108
|
+
// Process CSS with PostCSS + Tailwind
|
109
|
+
const result = await postcss([
|
110
|
+
tailwindcss,
|
111
|
+
autoprefixer
|
112
|
+
]).process(css, {
|
113
|
+
from: args.path,
|
114
|
+
to: path.join(buildDir, 'styles.css')
|
115
|
+
});
|
116
|
+
|
117
|
+
return {
|
118
|
+
contents: result.css,
|
119
|
+
loader: 'css'
|
120
|
+
};
|
121
|
+
} catch (error) {
|
122
|
+
console.error('Error processing CSS with Tailwind:', error);
|
123
|
+
return { contents: css, loader: 'css' };
|
124
|
+
}
|
125
|
+
});
|
126
|
+
}
|
127
|
+
};
|
128
|
+
} catch (error) {
|
129
|
+
console.warn('PostCSS plugins not available, CSS will not be processed with Tailwind:', error);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
esbuildContext = await esbuild.context({
|
134
|
+
entryPoints: [
|
135
|
+
path.join(__dirname, 'src', 'main.tsx'),
|
136
|
+
],
|
137
|
+
bundle: true,
|
138
|
+
format: 'esm',
|
139
|
+
outdir: buildDir,
|
140
|
+
sourcemap: isDev,
|
141
|
+
minify: !isDev,
|
142
|
+
jsx: 'transform',
|
143
|
+
jsxFactory: 'jsx',
|
144
|
+
jsxFragment: 'Fragment',
|
145
|
+
external: ['react', 'react/jsx-runtime', 'react-dom'],
|
146
|
+
define: {
|
147
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
148
|
+
},
|
149
|
+
loader: {
|
150
|
+
'.tsx': 'tsx',
|
151
|
+
'.ts': 'ts',
|
152
|
+
'.jsx': 'jsx',
|
153
|
+
'.js': 'js',
|
154
|
+
'.css': 'css',
|
155
|
+
'.json': 'json',
|
156
|
+
'.png': 'file',
|
157
|
+
'.jpg': 'file',
|
158
|
+
'.svg': 'file'
|
159
|
+
},
|
160
|
+
plugins: [
|
161
|
+
{
|
162
|
+
name: 'jsx-runtime-import',
|
163
|
+
setup(build) {
|
164
|
+
build.onLoad({ filter: /\.(tsx|jsx)$/ }, async (args) => {
|
165
|
+
const source = await fs.readFile(args.path, 'utf8');
|
166
|
+
const content = `import { jsx, Fragment } from 'frontend-hamroun';\n${source}`;
|
167
|
+
return { contents: content, loader: args.path.endsWith('tsx') ? 'tsx' : 'jsx' };
|
168
|
+
});
|
169
|
+
}
|
170
|
+
},
|
171
|
+
...(postcssPlugin ? [postcssPlugin] : [])
|
172
|
+
]
|
173
|
+
});
|
174
|
+
|
175
|
+
if (isDev) {
|
176
|
+
await esbuildContext.rebuild();
|
177
|
+
console.log('esbuild: Initial build complete');
|
178
|
+
} else {
|
179
|
+
await esbuildContext.rebuild();
|
180
|
+
await esbuildContext.dispose();
|
181
|
+
console.log('esbuild: Production build complete');
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
185
|
+
// Initialize esbuild
|
186
|
+
setupEsbuild().catch(err => {
|
187
|
+
console.error('Failed to setup esbuild:', err);
|
188
|
+
process.exit(1);
|
189
|
+
});
|
190
|
+
|
191
|
+
// Serve static files
|
192
|
+
app.use('/build', express.static(path.join(__dirname, 'build')));
|
193
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
194
|
+
|
195
|
+
// Development mode file serving
|
196
|
+
if (isDev) {
|
197
|
+
app.get('/src/*', async (req, res, next) => {
|
198
|
+
try {
|
199
|
+
const filePath = path.join(__dirname, req.path);
|
200
|
+
|
201
|
+
// Check if file exists
|
202
|
+
if (!existsSync(filePath)) {
|
203
|
+
return next();
|
204
|
+
}
|
205
|
+
|
206
|
+
// Read the file
|
207
|
+
const source = await fs.readFile(filePath, 'utf8');
|
208
|
+
|
209
|
+
// Determine content type based on file extension
|
210
|
+
const ext = path.extname(filePath);
|
211
|
+
let contentType = 'application/javascript';
|
212
|
+
let loader: esbuild.Loader = 'js';
|
213
|
+
|
214
|
+
switch (ext) {
|
215
|
+
case '.ts':
|
216
|
+
loader = 'ts';
|
217
|
+
break;
|
218
|
+
case '.tsx':
|
219
|
+
loader = 'tsx';
|
220
|
+
break;
|
221
|
+
case '.jsx':
|
222
|
+
loader = 'jsx';
|
223
|
+
break;
|
224
|
+
case '.css':
|
225
|
+
contentType = 'text/css';
|
226
|
+
loader = 'css';
|
227
|
+
break;
|
228
|
+
case '.json':
|
229
|
+
contentType = 'application/json';
|
230
|
+
loader = 'json';
|
231
|
+
break;
|
232
|
+
}
|
233
|
+
|
234
|
+
// Transform the file with esbuild
|
235
|
+
const result = await esbuild.transform(source, {
|
236
|
+
loader,
|
237
|
+
sourcemap: 'inline',
|
238
|
+
jsxFactory: 'jsx',
|
239
|
+
jsxFragment: 'Fragment'
|
240
|
+
});
|
241
|
+
|
242
|
+
res.setHeader('Content-Type', contentType);
|
243
|
+
res.send(result.code);
|
244
|
+
} catch (error) {
|
245
|
+
console.error(`Error serving ${req.path}:`, error);
|
246
|
+
next(error);
|
247
|
+
}
|
248
|
+
});
|
249
|
+
}
|
250
|
+
|
251
|
+
// Helper function to check if file exists
|
252
|
+
async function fileExists(filepath: string): Promise<boolean> {
|
32
253
|
try {
|
33
|
-
|
34
|
-
|
35
|
-
|
254
|
+
await fs.access(filepath);
|
255
|
+
return true;
|
256
|
+
} catch {
|
257
|
+
return false;
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
// SSR handler for all routes except /api and static files
|
262
|
+
app.get('*', async (req: Request, res: Response, next: NextFunction) => {
|
263
|
+
// Skip API routes, static assets, and build/src files
|
264
|
+
if (req.path.startsWith('/api/') ||
|
265
|
+
req.path.startsWith('/build/') ||
|
266
|
+
req.path.startsWith('/src/') ||
|
267
|
+
req.path.match(/\.(js|css|ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/)) {
|
268
|
+
return next();
|
269
|
+
}
|
270
|
+
|
271
|
+
console.log(`[SSR] Processing request for: ${req.path}`);
|
272
|
+
|
273
|
+
try {
|
274
|
+
// Normalize the path
|
275
|
+
const pagePath = req.path === '/' ? 'index' : req.path.slice(1);
|
276
|
+
|
277
|
+
// Create initial state
|
278
|
+
const initialState = {
|
279
|
+
route: req.path,
|
280
|
+
timestamp: new Date().toISOString(),
|
281
|
+
serverRendered: true,
|
282
|
+
data: {
|
283
|
+
users: store.users || [],
|
284
|
+
posts: store.posts || []
|
285
|
+
}
|
286
|
+
};
|
36
287
|
|
37
|
-
//
|
38
|
-
const
|
39
|
-
<div
|
40
|
-
<h1>
|
41
|
-
<p>
|
42
|
-
<div class="timestamp">Rendered at: ${new Date().toISOString()}</div>
|
43
|
-
<div class="api-container"></div>
|
288
|
+
// Generate HTML content directly as a string instead of JSX
|
289
|
+
const rootContent = `
|
290
|
+
<div style="padding: 20px; max-width: 800px; margin: 0 auto;">
|
291
|
+
<h1>Loading Application...</h1>
|
292
|
+
<p>The application is initializing. If you continue to see this message, please ensure JavaScript is enabled in your browser.</p>
|
44
293
|
</div>
|
45
294
|
`;
|
46
295
|
|
47
|
-
//
|
48
|
-
const
|
49
|
-
|
50
|
-
|
51
|
-
|
296
|
+
// Create HTML template with properly structured content
|
297
|
+
const html = `<!DOCTYPE html>
|
298
|
+
<html lang="en">
|
299
|
+
<head>
|
300
|
+
<meta charset="UTF-8">
|
301
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
302
|
+
<title>Frontend Hamroun App</title>
|
303
|
+
<!-- Include the processed Tailwind CSS file -->
|
304
|
+
<link rel="stylesheet" href="/styles.css">
|
305
|
+
<style>
|
306
|
+
body {
|
307
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
308
|
+
line-height: 1.6;
|
309
|
+
color: #333;
|
310
|
+
margin: 0;
|
311
|
+
padding: 0;
|
312
|
+
}
|
313
|
+
a {
|
314
|
+
color: #0066cc;
|
315
|
+
text-decoration: none;
|
316
|
+
}
|
317
|
+
a:hover {
|
318
|
+
text-decoration: underline;
|
319
|
+
}
|
320
|
+
pre {
|
321
|
+
white-space: pre-wrap;
|
322
|
+
word-break: break-word;
|
323
|
+
}
|
324
|
+
</style>
|
325
|
+
<script>
|
326
|
+
window.__INITIAL_STATE__ = ` + JSON.stringify(initialState) + `;
|
327
|
+
</script>
|
328
|
+
</head>
|
329
|
+
<body>
|
330
|
+
<div id="root">${rootContent}</div>
|
331
|
+
<script src="/build/main.js" type="module"></script>
|
332
|
+
<script src="/socket.io/socket.io.js"></script>
|
333
|
+
</body>
|
334
|
+
</html>`;
|
52
335
|
|
53
|
-
|
336
|
+
// Send the rendered HTML
|
337
|
+
res.send(html);
|
54
338
|
} catch (error) {
|
55
|
-
console.error('
|
56
|
-
|
57
|
-
<html>
|
58
|
-
<head><title>Server Error</title></head>
|
59
|
-
<body>
|
60
|
-
<h1>500 - Server Error</h1>
|
61
|
-
<p>${error.message}</p>
|
62
|
-
</body>
|
63
|
-
</html>
|
64
|
-
`);
|
339
|
+
console.error('[SSR] Server error:', error);
|
340
|
+
next(error);
|
65
341
|
}
|
66
342
|
});
|
67
343
|
|
344
|
+
// Error handler
|
345
|
+
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
346
|
+
console.error('Server error:', err);
|
347
|
+
res.status(500).send(`
|
348
|
+
<!DOCTYPE html>
|
349
|
+
<html>
|
350
|
+
<head><title>Server Error</title></head>
|
351
|
+
<body>
|
352
|
+
<h1>500 - Server Error</h1>
|
353
|
+
<p>${err.message}</p>
|
354
|
+
${isDev ? `<pre>${err.stack}</pre>` : ''}
|
355
|
+
</body>
|
356
|
+
</html>
|
357
|
+
`);
|
358
|
+
});
|
359
|
+
|
360
|
+
// WebSocket setup
|
361
|
+
io.on('connection', (socket) => {
|
362
|
+
console.log('Client connected:', socket.id);
|
363
|
+
|
364
|
+
// Send welcome message
|
365
|
+
socket.emit('welcome', {
|
366
|
+
message: 'Connected to WebSocket server',
|
367
|
+
time: new Date().toISOString()
|
368
|
+
});
|
369
|
+
|
370
|
+
// Handle client messages
|
371
|
+
socket.on('message', (data) => {
|
372
|
+
console.log('Received message:', data);
|
373
|
+
// Broadcast to all clients
|
374
|
+
io.emit('broadcast', {
|
375
|
+
from: socket.id,
|
376
|
+
data,
|
377
|
+
time: new Date().toISOString()
|
378
|
+
});
|
379
|
+
});
|
380
|
+
|
381
|
+
// Handle disconnect
|
382
|
+
socket.on('disconnect', () => {
|
383
|
+
console.log('Client disconnected:', socket.id);
|
384
|
+
});
|
385
|
+
});
|
386
|
+
|
387
|
+
// Handle file changes for live reload in development
|
388
|
+
if (isDev) {
|
389
|
+
const chokidar = await import('chokidar');
|
390
|
+
const watcher = chokidar.watch([
|
391
|
+
path.join(__dirname, 'src/**/*'),
|
392
|
+
path.join(__dirname, 'public/**/*')
|
393
|
+
]);
|
394
|
+
|
395
|
+
watcher.on('change', async (changedPath) => {
|
396
|
+
console.log(`[Watcher] File changed: ${changedPath}`);
|
397
|
+
|
398
|
+
// Rebuild if source files changed
|
399
|
+
if (changedPath.startsWith(path.join(__dirname, 'src'))) {
|
400
|
+
try {
|
401
|
+
await esbuildContext.rebuild();
|
402
|
+
console.log('[Watcher] Rebuild complete');
|
403
|
+
|
404
|
+
// Notify clients to reload
|
405
|
+
io.emit('reload');
|
406
|
+
} catch (error) {
|
407
|
+
console.error('[Watcher] Rebuild failed:', error);
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
// Notify clients of public file changes without rebuild
|
412
|
+
if (changedPath.startsWith(path.join(__dirname, 'public'))) {
|
413
|
+
io.emit('reload');
|
414
|
+
}
|
415
|
+
});
|
416
|
+
|
417
|
+
console.log('[Watcher] File watching enabled for development');
|
418
|
+
}
|
419
|
+
|
68
420
|
// Start the server
|
69
|
-
|
421
|
+
httpServer.listen(PORT, () => {
|
70
422
|
console.log(`Server running at http://localhost:${PORT}`);
|
423
|
+
console.log(`Server-side rendering enabled with direct esbuild compilation`);
|
424
|
+
|
425
|
+
if (isDev) {
|
426
|
+
console.log(`Development mode with live reload active`);
|
427
|
+
}
|
71
428
|
});
|
72
429
|
|
73
430
|
// Handle graceful shutdown
|
74
|
-
process.on('SIGINT', () => {
|
431
|
+
process.on('SIGINT', async () => {
|
75
432
|
console.log('Shutting down server...');
|
433
|
+
|
434
|
+
// Dispose esbuild context if active
|
435
|
+
if (esbuildContext) {
|
436
|
+
await esbuildContext.dispose();
|
437
|
+
}
|
438
|
+
|
76
439
|
process.exit(0);
|
77
440
|
});
|