redweb 0.9.0 → 0.11.0

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 (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +187 -23
  3. package/docs/LIVE_HTML.md +352 -0
  4. package/examples/live-html/cards.css +36 -0
  5. package/examples/live-html/cards.html +11 -0
  6. package/examples/live-html/cards.js +91 -0
  7. package/examples/live-html/cards.ts +35 -0
  8. package/examples/live-html/chatroom.css +156 -0
  9. package/examples/live-html/chatroom.js +268 -0
  10. package/examples/live-html/chatroom.ts +217 -0
  11. package/examples/live-html/components.css +7 -0
  12. package/examples/live-html/components.js +113 -0
  13. package/examples/live-html/components.ts +41 -0
  14. package/examples/live-html/counter.css +24 -0
  15. package/examples/live-html/counter.html +10 -0
  16. package/examples/live-html/counter.js +73 -0
  17. package/examples/live-html/counter.ts +21 -0
  18. package/examples/live-html/jsx-page.js +81 -0
  19. package/examples/live-html/jsx-page.tsx +41 -0
  20. package/examples/live-html/tsconfig.json +19 -0
  21. package/index.d.ts +219 -1
  22. package/index.js +18 -1
  23. package/jsx-dev-runtime.d.ts +13 -0
  24. package/jsx-dev-runtime.js +9 -0
  25. package/jsx-runtime.d.ts +28 -0
  26. package/jsx-runtime.js +5 -0
  27. package/package.json +47 -3
  28. package/src/htmx/Html.js +137 -0
  29. package/src/htmx/HtmlRenderer.js +88 -0
  30. package/src/htmx/HtmlSyntax.js +168 -0
  31. package/src/htmx/Jsx.js +86 -0
  32. package/src/htmx/LiveHtmlServer.js +91 -0
  33. package/src/htmx/LivePage.js +232 -0
  34. package/src/htmx/PageAssetLoader.js +34 -0
  35. package/src/htmx/PageManager.js +435 -0
  36. package/src/htmx/StaticExporter.js +78 -0
  37. package/src/htmx/StaticSite.js +182 -0
  38. package/src/htmx/TemplateRenderer.js +231 -0
  39. package/src/htmx/browserRuntime.js +97 -0
  40. package/src/htmx/index.js +10 -0
  41. package/src/htmx/metadata.js +349 -0
  42. package/src/htmx/sourceRoot.js +28 -0
  43. package/src/htmx/start.js +17 -0
  44. package/src/htmx/synchronous.js +9 -0
  45. package/src/http/BaseHttpServer.js +0 -35
  46. package/src/ws/BaseSocketServer.js +4 -0
  47. package/src/htmx/HtmxRenderer.js +0 -73
  48. package/src/htmx/RedWebHtmxComponent.js +0 -11
@@ -0,0 +1,349 @@
1
+ const PAGE_METADATA = new WeakMap();
2
+ const STATE_METADATA = new WeakMap();
3
+ const ACTION_METADATA = new WeakMap();
4
+ const RESOLVED_STATE = new WeakMap();
5
+ const RESOLVED_ACTION = new WeakMap();
6
+ const STANDARD_ACTIONS = new WeakMap();
7
+ const VIEW_METADATA = new WeakMap();
8
+ const RESOLVED_VIEW = new WeakMap();
9
+ const STANDARD_VIEWS = new WeakMap();
10
+ const PAGE_ROOTS = new WeakMap();
11
+ const PAGE_STYLESHEET_ROOTS = new WeakMap();
12
+ const COMPONENT_CLASSES = new WeakSet();
13
+ const { decoratorDirectory } = require('./sourceRoot');
14
+ const synchronous = require('./synchronous');
15
+ let metadataVersion = 0;
16
+
17
+ function assertDecoratorTarget(target, label) {
18
+ if (!target || typeof target !== 'object' || typeof target.constructor !== 'function') {
19
+ throw new TypeError(`${label} must decorate a class member.`);
20
+ }
21
+ return target.constructor;
22
+ }
23
+
24
+ function hierarchy(PageClass) {
25
+ const classes = [];
26
+ for (let current = PageClass; typeof current === 'function' && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
27
+ classes.unshift(current);
28
+ }
29
+ return classes;
30
+ }
31
+
32
+ function registerState(PageClass, property, config) {
33
+ const properties = new Map(STATE_METADATA.get(PageClass) || []);
34
+ const existing = properties.get(property);
35
+ if (existing?.writable === config.writable) return;
36
+ properties.set(property, config);
37
+ STATE_METADATA.set(PageClass, properties);
38
+ metadataVersion += 1;
39
+ }
40
+
41
+ function registerAction(PageClass, method, implementation) {
42
+ const methods = new Map(ACTION_METADATA.get(PageClass) || []);
43
+ if (methods.get(method) === implementation) return;
44
+ methods.set(method, implementation);
45
+ ACTION_METADATA.set(PageClass, methods);
46
+ metadataVersion += 1;
47
+ }
48
+
49
+ function registerStandardAction(PageClass, method, implementation) {
50
+ const methods = new Map(STANDARD_ACTIONS.get(PageClass) || []);
51
+ if (methods.get(method) === implementation) return;
52
+ methods.set(method, implementation);
53
+ STANDARD_ACTIONS.set(PageClass, methods);
54
+ metadataVersion += 1;
55
+ }
56
+
57
+ function registerView(store, PageClass, stateName, method, implementation) {
58
+ const views = new Map(store.get(PageClass) || []);
59
+ const existing = views.get(stateName);
60
+ if (existing?.method === method && existing.implementation === implementation) return;
61
+ views.set(stateName, Object.freeze({ method, implementation }));
62
+ store.set(PageClass, views);
63
+ metadataVersion += 1;
64
+ }
65
+
66
+ function resolvedState(PageClass) {
67
+ const cached = RESOLVED_STATE.get(PageClass);
68
+ if (cached?.version === metadataVersion) return cached.value;
69
+ const value = new Map();
70
+ hierarchy(PageClass).forEach(CurrentClass => {
71
+ STATE_METADATA.get(CurrentClass)?.forEach((config, property) => value.set(property, config));
72
+ });
73
+ RESOLVED_STATE.set(PageClass, { version: metadataVersion, value });
74
+ return value;
75
+ }
76
+
77
+ function resolvedAction(PageClass) {
78
+ const cached = RESOLVED_ACTION.get(PageClass);
79
+ if (cached?.version === metadataVersion) return cached.value;
80
+ const value = new Map();
81
+ hierarchy(PageClass).forEach(CurrentClass => {
82
+ const own = new Map(ACTION_METADATA.get(CurrentClass) || []);
83
+ STANDARD_ACTIONS.get(CurrentClass)?.forEach((implementation, method) => {
84
+ if (CurrentClass.prototype[method] === implementation) own.set(method, implementation);
85
+ });
86
+ value.forEach((_implementation, method) => {
87
+ if (Object.prototype.hasOwnProperty.call(CurrentClass.prototype, method) && !own.has(method)) value.delete(method);
88
+ });
89
+ own.forEach((implementation, method) => value.set(method, implementation));
90
+ });
91
+ RESOLVED_ACTION.set(PageClass, { version: metadataVersion, value });
92
+ return value;
93
+ }
94
+
95
+ function resolvedView(PageClass) {
96
+ const cached = RESOLVED_VIEW.get(PageClass);
97
+ if (cached?.version === metadataVersion) return cached.value;
98
+ const value = new Map();
99
+ hierarchy(PageClass).forEach(CurrentClass => {
100
+ const own = new Map(VIEW_METADATA.get(CurrentClass) || []);
101
+ STANDARD_VIEWS.get(CurrentClass)?.forEach((entry, stateName) => {
102
+ if (CurrentClass.prototype[entry.method] === entry.implementation) own.set(stateName, entry);
103
+ });
104
+ value.forEach((entry, stateName) => {
105
+ if (Object.prototype.hasOwnProperty.call(CurrentClass.prototype, entry.method) && !own.has(stateName)) value.delete(stateName);
106
+ });
107
+ own.forEach((entry, stateName) => value.set(stateName, entry));
108
+ });
109
+ RESOLVED_VIEW.set(PageClass, { version: metadataVersion, value });
110
+ return value;
111
+ }
112
+
113
+ function pageHead(value) {
114
+ if (value === undefined) return undefined;
115
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Page head must be an object.');
116
+ const allowed = new Set(['title', 'description', 'canonical', 'image', 'robots']);
117
+ const unknown = Object.keys(value).find(name => !allowed.has(name));
118
+ if (unknown) throw new TypeError(`Unknown page head option: ${unknown}.`);
119
+ const head = {};
120
+ for (const name of ['title', 'description', 'robots']) {
121
+ if (value[name] !== undefined && (typeof value[name] !== 'string' || !value[name])) {
122
+ throw new TypeError(`Page head ${name} must be a non-empty string.`);
123
+ }
124
+ if (value[name] !== undefined) head[name] = value[name];
125
+ }
126
+ for (const name of ['canonical', 'image']) {
127
+ if (value[name] === undefined) continue;
128
+ if (typeof value[name] !== 'string' || !value[name]) throw new TypeError(`Page head ${name} must be an absolute HTTP(S) URL.`);
129
+ let parsed;
130
+ try { parsed = new URL(value[name]); }
131
+ catch { throw new TypeError(`Page head ${name} must be an absolute HTTP(S) URL.`); }
132
+ if (!['http:', 'https:'].includes(parsed.protocol)) throw new TypeError(`Page head ${name} must be an absolute HTTP(S) URL.`);
133
+ head[name] = parsed.href;
134
+ }
135
+ return Object.freeze(head);
136
+ }
137
+
138
+ function pageCache(value, live) {
139
+ if (value === undefined) return undefined;
140
+ if (live) throw new TypeError('Page cache is available only when live is false.');
141
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Page cache must be an object.');
142
+ const allowed = new Set(['maxAge', 'staleWhileRevalidate', 'immutable']);
143
+ const unknown = Object.keys(value).find(name => !allowed.has(name));
144
+ if (unknown) throw new TypeError(`Unknown page cache option: ${unknown}.`);
145
+ const { maxAge = 0, staleWhileRevalidate = 0, immutable = false } = value;
146
+ if (!Number.isInteger(maxAge) || maxAge < 0) throw new TypeError('Page cache maxAge must be a non-negative integer.');
147
+ if (!Number.isInteger(staleWhileRevalidate) || staleWhileRevalidate < 0) {
148
+ throw new TypeError('Page cache staleWhileRevalidate must be a non-negative integer.');
149
+ }
150
+ if (typeof immutable !== 'boolean') throw new TypeError('Page cache immutable must be a boolean.');
151
+ return Object.freeze({ maxAge, staleWhileRevalidate, immutable });
152
+ }
153
+
154
+ function page(routePath, options = {}) {
155
+ const templateRoot = decoratorDirectory();
156
+ if (typeof routePath !== 'string' || !routePath.startsWith('/')) {
157
+ throw new TypeError('A page path beginning with "/" is required.');
158
+ }
159
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
160
+ throw new TypeError('Page options must be an object.');
161
+ }
162
+ const { template, css, shared, scope = shared ? 'shared' : 'connection', live = true, layout } = options;
163
+ if (template !== undefined && (typeof template !== 'string' || !template)) {
164
+ throw new TypeError('Page template must be a non-empty path.');
165
+ }
166
+ const stylesheets = css === undefined ? undefined : [...new Set(Array.isArray(css) ? css : [css])];
167
+ if (stylesheets && (stylesheets.length === 0 || stylesheets.some(file => typeof file !== 'string' || !file))) {
168
+ throw new TypeError('Page css must be a non-empty path or array of non-empty paths.');
169
+ }
170
+ if (shared !== undefined && typeof shared !== 'boolean') {
171
+ throw new TypeError('Page shared must be a boolean.');
172
+ }
173
+ if (shared !== undefined && options.scope !== undefined && (scope === 'shared') !== shared) {
174
+ throw new TypeError('Page scope and shared options conflict.');
175
+ }
176
+ if (!['connection', 'shared'].includes(scope)) {
177
+ throw new TypeError('Page scope must be "connection" or "shared".');
178
+ }
179
+ if (typeof live !== 'boolean') throw new TypeError('Page live must be a boolean.');
180
+ if (layout !== undefined && typeof layout !== 'function') throw new TypeError('Page layout must be a function.');
181
+ const head = pageHead(options.head);
182
+ const cache = pageCache(options.cache, live);
183
+ return PageClass => {
184
+ if (typeof PageClass !== 'function') throw new TypeError('page() must decorate a class.');
185
+ PAGE_METADATA.set(PageClass, Object.freeze({
186
+ path: routePath,
187
+ template,
188
+ scope,
189
+ ...(live === false && { live: false }),
190
+ ...(head && { head }),
191
+ ...(cache && { cache }),
192
+ ...(stylesheets && { css: Object.freeze(stylesheets) }),
193
+ ...(layout && { layout }),
194
+ }));
195
+ PAGE_ROOTS.set(PageClass, templateRoot);
196
+ return PageClass;
197
+ };
198
+ }
199
+
200
+ function component(render) {
201
+ if (render !== undefined) {
202
+ if (typeof render !== 'function') throw new TypeError('component() requires a render function.');
203
+ return function FunctionalComponent(properties) {
204
+ const result = synchronous(render(properties), 'Function components must render synchronously.');
205
+ const { isHtml } = require('./Html');
206
+ if (!isHtml(result)) throw new TypeError('Function components must return html.');
207
+ return result;
208
+ };
209
+ }
210
+ return ComponentClass => {
211
+ if (typeof ComponentClass !== 'function') throw new TypeError('component() must decorate a class.');
212
+ COMPONENT_CLASSES.add(ComponentClass);
213
+ return ComponentClass;
214
+ };
215
+ }
216
+
217
+ function isComponentClass(ComponentClass) {
218
+ return hierarchy(ComponentClass).some(CurrentClass => COMPONENT_CLASSES.has(CurrentClass));
219
+ }
220
+
221
+ function state(options = {}) {
222
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
223
+ throw new TypeError('State options must be an object.');
224
+ }
225
+ const { writable = false } = options;
226
+ if (typeof writable !== 'boolean') throw new TypeError('State writable must be a boolean.');
227
+ const config = Object.freeze({ writable });
228
+ return (target, property) => {
229
+ if (property?.kind === 'field') {
230
+ if (property.static || property.private || typeof property.name !== 'string' || !property.name) {
231
+ throw new TypeError('state() requires a public instance field with a string name.');
232
+ }
233
+ property.addInitializer(function registerStandardState() {
234
+ registerState(this.constructor, property.name, config);
235
+ });
236
+ return initialValue => initialValue;
237
+ }
238
+ const PageClass = assertDecoratorTarget(target, 'state()');
239
+ if (typeof property !== 'string' || !property) throw new TypeError('State property must be a non-empty string.');
240
+ registerState(PageClass, property, config);
241
+ };
242
+ }
243
+
244
+ function action() {
245
+ return (target, method, descriptor) => {
246
+ if (method?.kind === 'method') {
247
+ if (method.static || method.private || typeof method.name !== 'string' || !method.name || typeof target !== 'function') {
248
+ throw new TypeError('action() requires a public instance method with a string name.');
249
+ }
250
+ method.addInitializer(function registerStandardActionInitializer() {
251
+ if (this[method.name] === target) registerStandardAction(this.constructor, method.name, target);
252
+ });
253
+ return target;
254
+ }
255
+ const PageClass = assertDecoratorTarget(target, 'action()');
256
+ if (typeof method !== 'string' || !method || typeof descriptor?.value !== 'function') {
257
+ throw new TypeError('action() must decorate a method.');
258
+ }
259
+ registerAction(PageClass, method, descriptor.value);
260
+ return descriptor;
261
+ };
262
+ }
263
+
264
+ function view(stateName) {
265
+ if (typeof stateName !== 'string' || !stateName) throw new TypeError('view() requires a non-empty state name.');
266
+ return (target, method, descriptor) => {
267
+ if (method?.kind === 'method') {
268
+ if (method.static || method.private || typeof method.name !== 'string' || !method.name || typeof target !== 'function') {
269
+ throw new TypeError('view() requires a public instance method with a string name.');
270
+ }
271
+ method.addInitializer(function registerStandardViewInitializer() {
272
+ if (this[method.name] === target) registerView(STANDARD_VIEWS, this.constructor, stateName, method.name, target);
273
+ });
274
+ return target;
275
+ }
276
+ const PageClass = assertDecoratorTarget(target, 'view()');
277
+ if (typeof method !== 'string' || !method || typeof descriptor?.value !== 'function') {
278
+ throw new TypeError('view() must decorate a method.');
279
+ }
280
+ registerView(VIEW_METADATA, PageClass, stateName, method, descriptor.value);
281
+ return descriptor;
282
+ };
283
+ }
284
+
285
+ function getPageMetadata(PageClass) {
286
+ return PAGE_METADATA.get(PageClass);
287
+ }
288
+
289
+ function getPageTemplateRoot(PageClass) {
290
+ return PAGE_ROOTS.get(PageClass);
291
+ }
292
+
293
+ function getPageStylesheetRoots(PageClass) {
294
+ return PAGE_STYLESHEET_ROOTS.get(PageClass);
295
+ }
296
+
297
+ function setPageStylesheetRoots(PageClass, roots) {
298
+ PAGE_STYLESHEET_ROOTS.set(PageClass, Object.freeze([...roots]));
299
+ }
300
+
301
+ function getStateMetadata(PageClass) {
302
+ return new Map(resolvedState(PageClass));
303
+ }
304
+
305
+ function getActionMetadata(PageClass) {
306
+ return new Set(resolvedAction(PageClass).keys());
307
+ }
308
+
309
+ function getStateConfig(PageClass, property) {
310
+ return resolvedState(PageClass).get(property);
311
+ }
312
+
313
+ function forEachState(PageClass, callback) {
314
+ resolvedState(PageClass).forEach(callback);
315
+ }
316
+
317
+ function getActionImplementation(PageClass, method) {
318
+ return resolvedAction(PageClass).get(method);
319
+ }
320
+
321
+ function getViewImplementation(PageClass, stateName) {
322
+ return getViewMetadata(PageClass, stateName)?.implementation;
323
+ }
324
+
325
+ function getViewMetadata(PageClass, stateName) {
326
+ return resolvedView(PageClass).get(stateName);
327
+ }
328
+
329
+ module.exports = {
330
+ action,
331
+ component,
332
+ forEachState,
333
+ getActionImplementation,
334
+ getActionMetadata,
335
+ getPageMetadata,
336
+ getPageStylesheetRoots,
337
+ getPageTemplateRoot,
338
+ getStateConfig,
339
+ getStateMetadata,
340
+ getViewImplementation,
341
+ getViewMetadata,
342
+ isComponentClass,
343
+ page,
344
+ pageCache,
345
+ pageHead,
346
+ setPageStylesheetRoots,
347
+ state,
348
+ view,
349
+ };
@@ -0,0 +1,28 @@
1
+ const path = require('path');
2
+ const { fileURLToPath } = require('url');
3
+
4
+ function filePath(fileName) {
5
+ return fileName.startsWith('file:') ? fileURLToPath(fileName) : fileName;
6
+ }
7
+
8
+ function callerDirectory(frames, internalDirectory) {
9
+ const caller = frames.find(frame => {
10
+ const fileName = frame.getFileName?.();
11
+ return fileName && path.dirname(filePath(fileName)) !== internalDirectory;
12
+ });
13
+ return caller ? path.dirname(filePath(caller.getFileName())) : process.cwd();
14
+ }
15
+
16
+ function decoratorDirectory() {
17
+ const prepareStackTrace = Error.prepareStackTrace;
18
+ try {
19
+ Error.prepareStackTrace = (_error, frames) => frames;
20
+ const error = new Error();
21
+ Error.captureStackTrace(error, decoratorDirectory);
22
+ return callerDirectory(error.stack, __dirname);
23
+ } finally {
24
+ Error.prepareStackTrace = prepareStackTrace;
25
+ }
26
+ }
27
+
28
+ module.exports = { callerDirectory, decoratorDirectory, filePath };
@@ -0,0 +1,17 @@
1
+ const LiveHtmlServer = require('./LiveHtmlServer');
2
+
3
+ function start(pageOrPages, options = {}) {
4
+ const pages = Array.isArray(pageOrPages) ? pageOrPages : [pageOrPages];
5
+ if (!pages.length || pages.some(PageClass => typeof PageClass !== 'function')) {
6
+ throw new TypeError('start() requires a page class or a non-empty array of page classes.');
7
+ }
8
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
9
+ throw new TypeError('start() options must be an object.');
10
+ }
11
+ return new LiveHtmlServer({
12
+ ...options,
13
+ pages,
14
+ });
15
+ }
16
+
17
+ module.exports = { start };
@@ -0,0 +1,9 @@
1
+ function synchronous(result, message) {
2
+ if (result && typeof result.then === 'function') {
3
+ Promise.resolve(result).catch(() => {});
4
+ throw new TypeError(message);
5
+ }
6
+ return result;
7
+ }
8
+
9
+ module.exports = synchronous;
@@ -1,8 +1,6 @@
1
1
  const express = require('express');
2
2
  const path = require('path');
3
3
  const cors = require('cors');
4
- const fs = require('fs');
5
- const HtmxRenderer = require('../htmx/HtmxRenderer'); // Import the HtmxRenderer module
6
4
  const { validateListenerOptions } = require('../serverLifecycle');
7
5
 
8
6
  /**
@@ -24,7 +22,6 @@ const { validateListenerOptions } = require('../serverLifecycle');
24
22
  * @property {string} [ssl.cert] - Path to the SSL certificate file.
25
23
  * @property {import('express').Application} [server] - Existing Express application to configure.
26
24
  * @property {import('cors').CorsOptions} [corsOptions] - The CORS Options.
27
- * @property {boolean} [enableHtmxRendering=false] - Enable dynamic HTMX file rendering.
28
25
  */
29
26
 
30
27
  const ENCODINGS = { json: 'json', urlencoded: 'urlencoded' };
@@ -39,7 +36,6 @@ const HTTP_OPTIONS = {
39
36
  ssl: null,
40
37
  server: undefined,
41
38
  corsOptions: undefined,
42
- enableHtmxRendering: false, // New option for HTMX rendering
43
39
  exposeErrors: false,
44
40
  logger: console,
45
41
  };
@@ -75,11 +71,6 @@ function assertOptions(options) {
75
71
  }
76
72
  }
77
73
 
78
- function isWithin(root, candidate) {
79
- const relative = path.relative(root, candidate);
80
- return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
81
- }
82
-
83
74
  /**
84
75
  * Base HTTP Server
85
76
  * @param {RedWebOptions} options - Configuration options for RedWeb.
@@ -113,32 +104,6 @@ function BaseHttpServer(options = {}) {
113
104
  this.app.use(cors(this.options.corsOptions));
114
105
  }
115
106
 
116
- // Enable HTMX rendering if the flag is set
117
- if (this.enableHtmxRendering) {
118
- this.app.get('*.htmx', (req, res) => {
119
- const match = this.publicPaths
120
- .map(publicPath => {
121
- const root = path.resolve(process.cwd(), publicPath);
122
- const filePath = path.resolve(root, `.${req.path}`);
123
- return { root, filePath };
124
- })
125
- .find(({ root, filePath }) => isWithin(root, filePath) && fs.existsSync(filePath));
126
-
127
- if (!match) {
128
- return res.status(404).send('HTMX template not found');
129
- }
130
-
131
- try {
132
- const renderedContent = HtmxRenderer.render(match.filePath, { rootDir: match.root });
133
- res.type('html').send(renderedContent);
134
- } catch (error) {
135
- const message = this.exposeErrors ? `Error rendering HTMX file: ${error.message}` : 'Unable to render HTMX template';
136
- res.status(500).send(message);
137
- }
138
- });
139
- }
140
-
141
-
142
107
  // Serve static files from public paths
143
108
  this.publicPaths.forEach((publicPath) =>
144
109
  this.app.use(express.static(path.resolve(process.cwd(), publicPath)))
@@ -95,6 +95,10 @@ class BaseSocketServer {
95
95
  }
96
96
 
97
97
  handleUpgrade(req, sock, head) {
98
+ // Upgrade sockets are detached from Node's HTTP request lifecycle. A peer
99
+ // may reset one while admission is pending or after a rejection response;
100
+ // consume that transport-level event so it cannot crash the process.
101
+ if (typeof sock.on === 'function') sock.on('error', Function.prototype);
98
102
  // Some websocket clients (e.g., certain UE plugins) are finicky about the
99
103
  // HTTP upgrade path they send. Normalise the path and fall back to a default
100
104
  // route so we can still complete the upgrade instead of tearing the socket down.
@@ -1,73 +0,0 @@
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, { rootDir = path.dirname(path.resolve(filePath)), timeoutMs = 1000 } = {}) {
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 resolvedRoot = path.resolve(rootDir);
36
- const customRequire = (modulePath) => {
37
- if (typeof modulePath !== 'string' || !modulePath.startsWith('.')) {
38
- throw new Error('Templates may only require relative modules');
39
- }
40
- const absolutePath = path.resolve(path.dirname(filePath), modulePath);
41
- const relative = path.relative(resolvedRoot, absolutePath);
42
- if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
43
- throw new Error('Template module is outside the allowed root');
44
- }
45
- return require(absolutePath);
46
- };
47
-
48
- // Execute the script in a sandbox
49
- const script = new vm.Script(wrappedScript);
50
- const sandbox = {
51
- output: '',
52
- require: customRequire, // Add custom require
53
- __dirname: path.dirname(filePath),
54
- __filename: filePath,
55
- };
56
- vm.createContext(sandbox);
57
-
58
- // Get the rendered output
59
- let result = script.runInContext(sandbox, { timeout: timeoutMs });
60
-
61
- // Normalize spaces but preserve those in content
62
- result = result
63
- .replace(/>\s+</g, '><') // Remove spaces between tags
64
- .replace(/\s+/g, ' ') // Collapse multiple spaces to one
65
- .replace(/>\s+/g, '>') // Remove spaces after tags
66
- .replace(/\s+</g, '<') // Remove spaces before tags
67
- .trim(); // Trim leading and trailing spaces
68
-
69
- return result;
70
- }
71
- }
72
-
73
- module.exports = HtmxRenderer;
@@ -1,11 +0,0 @@
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;