redweb 0.6.8 → 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 +38 -18
- package/index.d.ts +6 -84
- package/package.json +1 -1
- package/src/htmx/HtmxRenderer.js +65 -0
- package/src/htmx/RedWebHtmxComponent.js +11 -0
- package/src/http/BaseHttpServer.js +39 -3
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
|
-
|
|
54
|
-
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;
|
|
70
|
-
handlers: Array<new () => BaseHandler>;
|
|
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
|
@@ -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;
|
|
@@ -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
|
-
|
|
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 =>
|
|
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
|
};
|