frontend-hamroun 1.2.27 → 1.2.29

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.
Files changed (41) hide show
  1. package/README.md +7 -0
  2. package/bin/banner.js +0 -0
  3. package/bin/cli-utils.js +0 -0
  4. package/bin/cli.js +536 -598
  5. package/package.json +1 -1
  6. package/templates/fullstack-app/README.md +37 -0
  7. package/templates/fullstack-app/build/main.css +664 -0
  8. package/templates/fullstack-app/build/main.css.map +7 -0
  9. package/templates/fullstack-app/build/main.js +682 -0
  10. package/templates/fullstack-app/build/main.js.map +7 -0
  11. package/templates/fullstack-app/build.ts +211 -0
  12. package/templates/fullstack-app/index.html +26 -3
  13. package/templates/fullstack-app/package-lock.json +2402 -438
  14. package/templates/fullstack-app/package.json +24 -9
  15. package/templates/fullstack-app/postcss.config.js +6 -0
  16. package/templates/fullstack-app/process-tailwind.js +45 -0
  17. package/templates/fullstack-app/public/_redirects +1 -0
  18. package/templates/fullstack-app/public/route-handler.js +47 -0
  19. package/templates/fullstack-app/public/spa-fix.html +17 -0
  20. package/templates/fullstack-app/public/styles.css +768 -0
  21. package/templates/fullstack-app/public/tailwind.css +15 -0
  22. package/templates/fullstack-app/server.js +101 -44
  23. package/templates/fullstack-app/server.ts +402 -39
  24. package/templates/fullstack-app/src/README.md +55 -0
  25. package/templates/fullstack-app/src/client.js +83 -16
  26. package/templates/fullstack-app/src/components/Layout.tsx +45 -0
  27. package/templates/fullstack-app/src/components/UserList.tsx +27 -0
  28. package/templates/fullstack-app/src/config.ts +42 -0
  29. package/templates/fullstack-app/src/data/api.ts +71 -0
  30. package/templates/fullstack-app/src/main.tsx +219 -7
  31. package/templates/fullstack-app/src/pages/about/index.tsx +67 -0
  32. package/templates/fullstack-app/src/pages/index.tsx +30 -60
  33. package/templates/fullstack-app/src/pages/users.tsx +60 -0
  34. package/templates/fullstack-app/src/router.ts +255 -0
  35. package/templates/fullstack-app/src/styles.css +5 -0
  36. package/templates/fullstack-app/tailwind.config.js +11 -0
  37. package/templates/fullstack-app/tsconfig.json +18 -0
  38. package/templates/fullstack-app/vite.config.js +53 -6
  39. package/templates/ssr-template/readme.md +50 -0
  40. package/templates/ssr-template/src/client.ts +46 -14
  41. 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 endpoints - Core data
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
- // Dynamic API route loading
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
- const entries = fs.readdirSync(dir, { withFileTypes: true });
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
- if (entry.isDirectory()) {
98
- // Process directories recursively
99
- await registerApiRoutes(fullPath, `${routePrefix}/${entry.name}`);
100
- } else if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
101
- // Process API file
102
- try {
103
- const fileName = entry.name.replace(/\.[^/.]+$/, ""); // Remove extension
104
- const routePath = fileName === 'index'
105
- ? routePrefix
106
- : fileName.startsWith('[') && fileName.endsWith(']')
107
- ? `${routePrefix}/:${fileName.slice(1, -1)}` // Convert [param] to :param
108
- : `${routePrefix}/${fileName}`;
109
-
110
- // Import the route module
111
- const module = await import(`file://${fullPath}`);
112
-
113
- // Register handlers for HTTP methods
114
- ['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
115
- if (typeof module[method] === 'function') {
116
- console.log(` Registered ${method.toUpperCase()} ${routePath}`);
117
- app[method](`/api${routePath}`, module[method]);
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
- try {
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 requests
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
- res.sendFile(indexPath);
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, redirect to Vite dev server
188
- res.redirect(`http://localhost:5173${req.path}`);
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
- // Get __dirname equivalent in ESM
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
- // Serve static files from current directory
15
- app.use(express.static(__dirname));
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
- // Parse JSON and URL-encoded data
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
- // Basic API endpoint
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
- // Serve index.html for all other routes
31
- app.get('*', (req, res) => {
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
- // Read index.html
34
- const indexPath = path.join(__dirname, 'index.html');
35
- const html = fs.readFileSync(indexPath, 'utf8');
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
- // Add server-rendered content
38
- const serverContent = `
39
- <div class="server-content">
40
- <h1>Frontend Hamroun Fullstack</h1>
41
- <p>Server-side rendered content</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
- // Replace placeholder with server content
48
- const modifiedHtml = html.replace(
49
- /<div class="placeholder">[\s\S]*?<\/div>/,
50
- serverContent
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
- res.send(modifiedHtml);
336
+ // Send the rendered HTML
337
+ res.send(html);
54
338
  } catch (error) {
55
- console.error('Error serving HTML:', error);
56
- res.status(500).send(`
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
- app.listen(PORT, () => {
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
  });