redweb 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,35 +28,53 @@ const socketServer = new SocketServer();
28
28
 
29
29
  ### Custom Configuration
30
30
 
31
- #### HTTP Server
31
+ #### HTTP Server with HTMX Rendering
32
+
33
+ RedWeb now supports dynamic rendering of `.htmx` files using `enableHtmxRendering`.
32
34
 
33
35
  ```javascript
34
36
  const { HttpServer, METHODS } = require('redweb');
35
37
 
36
- const services = [
37
- {
38
- serviceName: '/submit-form',
39
- method: METHODS.POST,
40
- function: (req, res) => {
41
- const { name, email, message } = req.body;
42
- if (!name || !email || !message) {
43
- return res.status(400).json({ error: 'All fields are required' });
44
- }
45
- res.status(200).json({ success: 'Form submitted successfully' });
46
- }
47
- }
48
- ];
49
-
50
38
  const options = {
51
39
  port: 3000,
52
40
  publicPaths: ['./public'],
53
- encoding: 'urlencoded',
54
- services: services
41
+ enableHtmxRendering: true, // Enable .htmx rendering
42
+ services: [
43
+ {
44
+ serviceName: '/submit-form',
45
+ method: METHODS.POST,
46
+ function: (req, res) => {
47
+ const { name, email, message } = req.body;
48
+ if (!name || !email || !message) {
49
+ return res.status(400).json({ error: 'All fields are required' });
50
+ }
51
+ res.status(200).json({ success: 'Form submitted successfully' });
52
+ }
53
+ }
54
+ ]
55
55
  };
56
56
 
57
57
  const app = new HttpServer(options);
58
58
  ```
59
59
 
60
+ Place `.htmx` files in the specified `publicPaths`, and they will be dynamically rendered.
61
+
62
+ Example:
63
+
64
+ **File: `public/index.htmx`**
65
+ ```javascript
66
+ const name = 'RedWeb User';
67
+
68
+ <@>
69
+ <h1>Hello, {{name}}!</h1>
70
+ <@/>
71
+ ```
72
+
73
+ Accessing `/index.htmx` will render:
74
+ ```html
75
+ <h1>Hello, RedWeb User!</h1>
76
+ ```
77
+
60
78
  #### HTTPS Server
61
79
 
62
80
  ```javascript
@@ -68,7 +86,8 @@ const options = {
68
86
  key: './path/to/key.pem',
69
87
  cert: './path/to/cert.pem'
70
88
  },
71
- publicPaths: ['./public']
89
+ publicPaths: ['./public'],
90
+ enableHtmxRendering: true // Enable .htmx rendering
72
91
  };
73
92
 
74
93
  const app = new HttpsServer(options);
@@ -207,6 +226,7 @@ module.exports = ChatRoute;
207
226
  - **listenCallback**: Function to execute once the server starts listening.
208
227
  - **encoding**: Encoding type for request bodies (`'json'` or `'urlencoded'`).
209
228
  - **ssl**: SSL configuration for HTTPS server (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
229
+ - **enableHtmxRendering**: Enable dynamic rendering of `.htmx` files (default: `false`).
210
230
 
211
231
  ### SocketServer Options
212
232
 
package/index.d.ts CHANGED
@@ -32,6 +32,8 @@ declare module 'redweb' {
32
32
  key: string;
33
33
  cert: string;
34
34
  };
35
+ corsOptions?: import('cors').CorsOptions;
36
+ enableHtmxRendering?: boolean; // New flag to enable HTMX rendering
35
37
  }
36
38
 
37
39
  /**
@@ -66,61 +68,23 @@ declare module 'redweb' {
66
68
  * WebSocket route configuration.
67
69
  */
68
70
  export interface SocketRouteConfig {
69
- path: string; // The WebSocket route path (e.g., "/chat").
70
- handlers: Array<new () => BaseHandler>; // Array of handler classes for the route.
71
+ path: string;
72
+ handlers: Array<new () => BaseHandler>;
71
73
  }
72
74
 
73
75
  /**
74
76
  * Represents a WebSocket route.
75
77
  */
76
78
  export class SocketRoute {
77
- /**
78
- * The path for the WebSocket route.
79
- */
80
79
  path: string;
81
-
82
- /**
83
- * Handlers associated with the route.
84
- */
85
80
  handlers: BaseHandler[];
86
81
 
87
- /**
88
- * Creates a new `SocketRoute` instance.
89
- * @param config - Configuration options for the route.
90
- */
91
82
  constructor(config: SocketRouteConfig);
92
83
 
93
- /**
94
- * Adds a new handler dynamically.
95
- * @param HandlerClass - A class extending `BaseHandler`.
96
- */
97
84
  addHandler(HandlerClass: new () => BaseHandler): void;
98
-
99
- /**
100
- * Handles a new WebSocket connection.
101
- * @param socket - The WebSocket connection instance.
102
- * @param req - The HTTP request associated with the connection.
103
- */
104
85
  handleConnection(socket: WebSocket, req: import('http').IncomingMessage): void;
105
-
106
- /**
107
- * Handles incoming WebSocket messages.
108
- * @param socket - The WebSocket connection instance.
109
- * @param data - The message data.
110
- */
111
86
  handleMessage(socket: WebSocket, data: any): void;
112
-
113
- /**
114
- * Handles WebSocket disconnections.
115
- * @param socket - The WebSocket connection instance.
116
- */
117
87
  handleClose(socket: WebSocket): void;
118
-
119
- /**
120
- * Handles WebSocket errors.
121
- * @param socket - The WebSocket connection instance.
122
- * @param error - The error object.
123
- */
124
88
  handleError(socket: WebSocket, error: Error): void;
125
89
  }
126
90
 
@@ -128,33 +92,11 @@ declare module 'redweb' {
128
92
  * Base class for WebSocket handlers.
129
93
  */
130
94
  export class BaseHandler {
131
- /**
132
- * The name of the handler (used to identify it in the server).
133
- */
134
95
  name: string;
135
- /**
136
- * Creates a new handler instance.
137
- * @param name - The name of the handler.
138
- */
139
- constructor(name: string);
140
96
 
141
- /**
142
- * Handles an incoming message.
143
- * @param socket - The WebSocket connection that sent the message.
144
- * @param message - The message data.
145
- */
97
+ constructor(name: string);
146
98
  onMessage(socket: WebSocket, message: Object): void;
147
-
148
- /**
149
- * Called during the first contact with a new WebSocket connection.
150
- * @param socket - The WebSocket connection instance.
151
- */
152
99
  onInitialContact(socket: WebSocket): void;
153
-
154
- /**
155
- * Called when a WebSocket connection closes.
156
- * @param socket - The WebSocket connection instance.
157
- */
158
100
  onClose(socket: WebSocket): void;
159
101
  }
160
102
 
@@ -162,31 +104,11 @@ declare module 'redweb' {
162
104
  * Base WebSocket server class.
163
105
  */
164
106
  export class BaseSocketServer {
165
- /**
166
- * List of WebSocket routes.
167
- */
168
107
  routes: SocketRoute[];
169
108
 
170
- /**
171
- * Creates a new `BaseSocketServer`.
172
- * @param server - The HTTP server instance.
173
- * @param options - Configuration options.
174
- */
175
109
  constructor(server: HTTPServer | HTTPSServer, options?: SocketServerOptions);
176
-
177
- /**
178
- * Handles WebSocket upgrade requests.
179
- * @param req - The incoming HTTP upgrade request.
180
- * @param socket - The raw network socket.
181
- * @param head - The initial data chunk.
182
- */
183
110
  handleUpgrade(req: import('http').IncomingMessage, socket: import('net').Socket, head: Buffer): void;
184
-
185
- /**
186
- *
187
- * @param route
188
- */
189
- addRoute(route: new () => SocketRoute);
111
+ addRoute(route: new () => SocketRoute): void;
190
112
  }
191
113
 
192
114
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "A way to quickly set up an express server",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const vm = require('vm');
3
+ const path = require('path');
4
+
5
+ class HtmxRenderer {
6
+ /**
7
+ * Render an .htmx file as JavaScript with embedded print statements.
8
+ * @param {string} filePath - Path to the .htmx file.
9
+ * @returns {string} Rendered HTML string with normalized whitespace.
10
+ */
11
+ static render(filePath) {
12
+ if (!fs.existsSync(filePath)) {
13
+ throw new Error(`Template file not found: ${filePath}`);
14
+ }
15
+
16
+ let output = '';
17
+ const templateContent = fs.readFileSync(filePath, 'utf-8');
18
+
19
+ // Transform <@ ... @/> blocks into print() calls
20
+ const transformedTemplate = templateContent.replace(
21
+ /<@>([\s\S]*?)<@\/>/g,
22
+ (_, content) => `print(\`${content.replace(/{{\s*(.*?)\s*}}/g, '${$1}')}\`);`
23
+ );
24
+
25
+ // Wrap the script in an IIFE
26
+ const wrappedScript = `
27
+ (() => {
28
+ const print = (html) => output += html;
29
+ ${transformedTemplate}
30
+ return output;
31
+ })();
32
+ `;
33
+
34
+ // Create a custom require function that resolves paths relative to the template
35
+ const customRequire = (modulePath) => {
36
+ const absolutePath = path.resolve(path.dirname(filePath), modulePath);
37
+ return require(absolutePath);
38
+ };
39
+
40
+ // Execute the script in a sandbox
41
+ const script = new vm.Script(wrappedScript);
42
+ const sandbox = {
43
+ output: '',
44
+ require: customRequire, // Add custom require
45
+ __dirname: path.dirname(filePath),
46
+ __filename: filePath,
47
+ };
48
+ vm.createContext(sandbox);
49
+
50
+ // Get the rendered output
51
+ let result = script.runInContext(sandbox);
52
+
53
+ // Normalize spaces but preserve those in content
54
+ result = result
55
+ .replace(/>\s+</g, '><') // Remove spaces between tags
56
+ .replace(/\s+/g, ' ') // Collapse multiple spaces to one
57
+ .replace(/>\s+/g, '>') // Remove spaces after tags
58
+ .replace(/\s+</g, '<') // Remove spaces before tags
59
+ .trim(); // Trim leading and trailing spaces
60
+
61
+ return result;
62
+ }
63
+ }
64
+
65
+ module.exports = HtmxRenderer;
@@ -0,0 +1,11 @@
1
+ class RedWebHtmxComponent {
2
+ constructor(props = {}) {
3
+ this.props = props;
4
+ }
5
+
6
+ render() {
7
+ throw new Error('Render method must be implemented in derived components');
8
+ }
9
+ }
10
+
11
+ module.exports = RedWebHtmxComponent;
@@ -2,6 +2,8 @@ const express = require('express');
2
2
  const bodyParser = require('body-parser');
3
3
  const path = require('path');
4
4
  const cors = require('cors');
5
+ const fs = require('fs');
6
+ const HtmxRenderer = require('../htmx/HtmxRenderer'); // Import the HtmxRenderer module
5
7
 
6
8
  /**
7
9
  * @typedef {'json' | 'urlencoded'} RedWebEncoding
@@ -21,6 +23,7 @@ const cors = require('cors');
21
23
  * @property {string} [ssl.cert] - Path to the SSL certificate file.
22
24
  * @property {import('express').Application} [server] - Whether to automatically start listening.
23
25
  * @property {import('cors').CorsOptions} [corsOptions] - The CORS Options.
26
+ * @property {boolean} [enableHtmxRendering=false] - Enable dynamic HTMX file rendering.
24
27
  */
25
28
 
26
29
  const ENCODINGS = { json: 'json', urlencoded: 'urlencoded' };
@@ -34,6 +37,7 @@ const HTTP_OPTIONS = {
34
37
  ssl: null,
35
38
  server: undefined,
36
39
  corsOptions: undefined,
40
+ enableHtmxRendering: false, // New option for HTMX rendering
37
41
  };
38
42
 
39
43
  /**
@@ -52,18 +56,50 @@ function BaseHttpServer(options = {}) {
52
56
  } else if (this.encoding === ENCODINGS.urlencoded) {
53
57
  this.app.use(bodyParser.urlencoded({ extended: true }));
54
58
  }
59
+
55
60
  this.app.use(cors(this.options.corsOptions));
56
- this.publicPaths.forEach(public_path => this.app.use(express.static(path.join(process.cwd(), public_path))));
61
+
62
+ // Enable HTMX rendering if the flag is set
63
+ if (this.enableHtmxRendering) {
64
+ this.app.get('*.htmx', (req, res) => {
65
+ // Find the file in one of the publicPaths
66
+ const filePath = this.publicPaths
67
+ .map(publicPath => path.join(process.cwd(), publicPath, req.path))
68
+ .find(fullPath => fs.existsSync(fullPath)); // Check if the file exists
69
+
70
+ if (!filePath) {
71
+ return res.status(404).send(`Error rendering HTMX file: Template file not found: ${req.path}`);
72
+ }
73
+
74
+ try {
75
+ const renderedContent = HtmxRenderer.render(filePath);
76
+ res.type('html').send(renderedContent);
77
+ } catch (error) {
78
+ res.status(500).send(`Error rendering HTMX file: ${error.message}`);
79
+ }
80
+ });
81
+ }
82
+
83
+
84
+ // Serve static files from public paths
85
+ this.publicPaths.forEach((publicPath) =>
86
+ this.app.use(express.static(path.join(process.cwd(), publicPath)))
87
+ );
88
+
57
89
  const catchAll = this.services.find((service) => service.serviceName === '*');
58
90
  if (catchAll) this.services.splice(this.services.indexOf(catchAll), 1);
59
- this.services.forEach(service => this.app[service.method](service.serviceName, service.function));
91
+ this.services.forEach((service) =>
92
+ this.app[service.method](service.serviceName, service.function)
93
+ );
60
94
  if (catchAll) this.app[catchAll.method](catchAll.serviceName, catchAll.function);
95
+
61
96
  return this;
62
97
  }
63
98
 
99
+
64
100
  module.exports = {
65
101
  BaseHttpServer,
66
102
  ENCODINGS,
67
103
  HTTP_OPTIONS,
68
- METHODS: {GET: 'get', POST: 'post', PUT: 'put', DELETE: 'delete'}
104
+ METHODS: { GET: 'get', POST: 'post', PUT: 'put', DELETE: 'delete' },
69
105
  };