@server/next 0.28.5 → 0.28.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.28.5",
3
+ "version": "0.28.7",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -9,7 +9,7 @@ const entities = {
9
9
  const encode = (str = "") => {
10
10
  if (typeof str === "number") str = String(str);
11
11
  if (typeof str !== "string") return "";
12
- return str.replace(/[&<>"]/g, (tag) => entities[tag]);
12
+ return str.replace(/[&<>"']/g, (tag) => entities[tag]);
13
13
  };
14
14
 
15
15
  const SELFCLOSE = new Set(
@@ -20,39 +20,52 @@ const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
20
20
 
21
21
  const altAttrs = {
22
22
  classname: "class",
23
+ htmlfor: "for",
23
24
  };
24
25
 
25
- // valid primitives only
26
- const isValidChild = (child) => child === 0 || child === "" || !!child;
27
-
28
- // CSS helpers
29
- const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
26
+ // valid primitives only — null, undefined, false, true are all skipped
27
+ const isValidChild = (child) =>
28
+ child != null && child !== false && child !== true;
30
29
 
31
30
  const minifyCss = (str) =>
32
31
  str
32
+ .replace(/\/\*[\s\S]*?\*\//g, "")
33
33
  .replace(/\s+/g, " ")
34
- .replace(/\/\*[^*]*\*\//g, "")
34
+ .replace(/\s*([{}:;,>~+])\s*/g, "$1")
35
+ .replace(/;}/g, "}")
36
+ .replace(/<\/style>/gi, "<\\/style>")
35
37
  .trim();
36
38
 
37
- // React element detection (safe for your custom objects too)
39
+ // React element detection (safe for custom objects too)
38
40
  const isReactElement = (val) =>
39
41
  val !== null && typeof val === "object" && "type" in val && "props" in val;
40
42
 
41
- // 🔥 CRITICAL: single-pass renderer (NO recursion cycles)
43
+ // Marker for raw (pre-escaped) content that bypasses HTML encoding
44
+ const RAW = Symbol("raw");
45
+ const raw = (str) => ({ [RAW]: String(str) });
46
+
42
47
  const renderChild = (child) => {
43
- if (child == null || child === false) return "";
48
+ if (child == null || child === false || child === true) return "";
44
49
 
45
50
  if (Array.isArray(child)) {
46
51
  return child.map(renderChild).join("");
47
52
  }
48
53
 
49
54
  if (typeof child === "function") {
50
- return renderChild(child());
55
+ const result = child();
56
+ // Strings returned by functions are pre-rendered HTML — don't re-encode them
57
+ if (result == null || result === false || result === true) return "";
58
+ if (typeof result === "string") return result;
59
+ if (typeof result === "number") return String(result);
60
+ return renderChild(result);
51
61
  }
52
62
 
53
- if (typeof child === "string") return child;
63
+ if (typeof child === "string") return encode(child);
54
64
  if (typeof child === "number") return String(child);
55
65
 
66
+ // Raw marker bypasses encoding (used by script, style, dangerouslySetInnerHTML)
67
+ if (typeof child === "object" && RAW in child) return child[RAW];
68
+
56
69
  if (isReactElement(child)) {
57
70
  return jsx(child.type, child.props || {})();
58
71
  }
@@ -62,7 +75,7 @@ const renderChild = (child) => {
62
75
  };
63
76
 
64
77
  const jsx = (tag, { children, ...props } = {}) => {
65
- // 🔥 Fragment (Symbol-safe)
78
+ // Fragment (Symbol-safe)
66
79
  if (tag === REACT_FRAGMENT_TYPE || tag === Fragment) {
67
80
  return () => renderChild(children);
68
81
  }
@@ -81,21 +94,24 @@ const jsx = (tag, { children, ...props } = {}) => {
81
94
  return () => renderChild(tag.render({ children, ...props }, null));
82
95
  }
83
96
 
84
- // script special-case
85
- if (tag === "script" && children) {
86
- const src = children;
87
- children = () => src;
97
+ // script: raw content (JS must not be HTML-encoded)
98
+ if (tag === "script" && children != null) {
99
+ const parts = Array.isArray(children) ? children : [children];
100
+ const content = parts
101
+ .filter(isValidChild)
102
+ .map((c) => (typeof c === "string" ? c : typeof c === "number" ? String(c) : ""))
103
+ .join("");
104
+ children = raw(content);
88
105
  }
89
106
 
90
- // style special-case
107
+ // style: minified raw content
91
108
  if (tag === "style" && typeof children === "string") {
92
- const src = minifyCss(escapeCSS(children));
93
- children = () => src;
109
+ children = raw(minifyCss(children));
94
110
  }
95
111
 
96
112
  // dangerouslySetInnerHTML
97
113
  if (props?.dangerouslySetInnerHTML) {
98
- children = () => props.dangerouslySetInnerHTML.__html;
114
+ children = raw(props.dangerouslySetInnerHTML.__html ?? "");
99
115
  }
100
116
 
101
117
  // normalize children
@@ -104,7 +120,7 @@ const jsx = (tag, { children, ...props } = {}) => {
104
120
  .map(renderChild)
105
121
  .join("");
106
122
 
107
- // 🔥 CRITICAL SAFETY GATE (prevents Symbol/string crash)
123
+ // safety gate (prevents Symbol/unknown-type crash)
108
124
  if (!tag || typeof tag !== "string") {
109
125
  return () => flatChildren;
110
126
  }
@@ -120,7 +136,7 @@ const jsx = (tag, { children, ...props } = {}) => {
120
136
  if (v === true) return key;
121
137
 
122
138
  const value =
123
- typeof v === "string" || typeof v === "number" ? encode(v) : "";
139
+ typeof v === "string" || typeof v === "number" ? encode(String(v)) : "";
124
140
 
125
141
  return `${key}="${value}"`;
126
142
  })
@@ -137,6 +153,6 @@ const jsx = (tag, { children, ...props } = {}) => {
137
153
  return () => `${doctype}<${tag}${attrStr}>${flatChildren}</${tag}>`;
138
154
  };
139
155
 
140
- const Fragment = "";
156
+ const Fragment = Symbol.for("react.fragment");
141
157
 
142
158
  export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment };
@@ -56,6 +56,7 @@ describe("jsx", () => {
56
56
  it("can render a pre with indented text", () => {
57
57
  expect(<pre>{` indented`}</pre>).toRender("<pre> indented</pre>");
58
58
  });
59
+
59
60
  it("can render a pre inside a div", () => {
60
61
  expect(
61
62
  <div>
@@ -63,6 +64,189 @@ describe("jsx", () => {
63
64
  </div>,
64
65
  ).toRender("<div><pre>line1\nline2</pre></div>");
65
66
  });
67
+
68
+ it("does NOT render the <pre>", () => {
69
+ expect(<code>{"<pre>"}</code>).toRender("<code>&lt;pre&gt;</code>");
70
+ });
71
+ });
72
+
73
+ describe("text encoding", () => {
74
+ it("encodes < and > to prevent XSS", () => {
75
+ expect(<div>{"<script>alert(1)</script>"}</div>).toRender(
76
+ "<div>&lt;script&gt;alert(1)&lt;/script&gt;</div>",
77
+ );
78
+ });
79
+
80
+ it("encodes & in text content", () => {
81
+ expect(<div>{"a & b"}</div>).toRender("<div>a &amp; b</div>");
82
+ });
83
+
84
+ it('encodes " in text content', () => {
85
+ expect(<div>{'say "hi"'}</div>).toRender("<div>say &quot;hi&quot;</div>");
86
+ });
87
+
88
+ it("encodes ' in text content", () => {
89
+ expect(<div>{"it's"}</div>).toRender("<div>it&#39;s</div>");
90
+ });
91
+
92
+ it("renders 0 as a child", () => {
93
+ expect(<div>{0}</div>).toRender("<div>0</div>");
94
+ });
95
+
96
+ it("renders positive integers", () => {
97
+ expect(<div>{42}</div>).toRender("<div>42</div>");
98
+ });
99
+
100
+ it("renders floats", () => {
101
+ expect(<div>{3.14}</div>).toRender("<div>3.14</div>");
102
+ });
103
+
104
+ it("renders negative numbers", () => {
105
+ expect(<div>{-1}</div>).toRender("<div>-1</div>");
106
+ });
107
+
108
+ it("renders nothing for null", () => {
109
+ expect(<div>{null}</div>).toRender("<div></div>");
110
+ });
111
+
112
+ it("renders nothing for undefined", () => {
113
+ expect(<div>{undefined}</div>).toRender("<div></div>");
114
+ });
115
+
116
+ it("renders nothing for false", () => {
117
+ expect(<div>{false}</div>).toRender("<div></div>");
118
+ });
119
+
120
+ it("renders nothing for true", () => {
121
+ expect(<div>{true}</div>).toRender("<div></div>");
122
+ });
123
+
124
+ it("renders conditional content", () => {
125
+ const show = true;
126
+ expect(<div>{show && <span>yes</span>}</div>).toRender(
127
+ "<div><span>yes</span></div>",
128
+ );
129
+ const hide = false;
130
+ expect(<div>{hide && <span>no</span>}</div>).toRender("<div></div>");
131
+ });
132
+
133
+ it("renders multiple children in order", () => {
134
+ expect(
135
+ <ul>
136
+ <li>a</li>
137
+ <li>b</li>
138
+ <li>c</li>
139
+ </ul>,
140
+ ).toRender("<ul><li>a</li><li>b</li><li>c</li></ul>");
141
+ });
142
+
143
+ it("renders array children", () => {
144
+ const items = ["x", "y", "z"];
145
+ expect(<span>{items.map((i) => <b key={i}>{i}</b>)}</span>).toRender(
146
+ "<span><b>x</b><b>y</b><b>z</b></span>",
147
+ );
148
+ });
149
+
150
+ it("treats string returned from a function child as raw HTML", () => {
151
+ const getHtml = () => "<b>bold</b>";
152
+ expect(<div>{getHtml}</div>).toRender("<div><b>bold</b></div>");
153
+ });
154
+ });
155
+
156
+ describe("attribute encoding", () => {
157
+ it("encodes < and > in attribute values", () => {
158
+ expect(<div title="<b>hi</b>"></div>).toRender(
159
+ '<div title="&lt;b&gt;hi&lt;/b&gt;"></div>',
160
+ );
161
+ });
162
+
163
+ it("encodes & in attribute values", () => {
164
+ expect(<div title="a & b"></div>).toRender('<div title="a &amp; b"></div>');
165
+ });
166
+
167
+ it('encodes " in attribute values', () => {
168
+ expect(<div title={`say "hi"`}></div>).toRender(
169
+ '<div title="say &quot;hi&quot;"></div>',
170
+ );
171
+ });
172
+
173
+ it("encodes ' in attribute values", () => {
174
+ expect(<div title={"it's"}></div>).toRender(
175
+ '<div title="it&#39;s"></div>',
176
+ );
177
+ });
178
+
179
+ it("maps className to class", () => {
180
+ expect(<div className="foo bar"></div>).toRender(
181
+ '<div class="foo bar"></div>',
182
+ );
183
+ });
184
+
185
+ it("maps htmlFor to for", () => {
186
+ expect(<label htmlFor="email"></label>).toRender(
187
+ '<label for="email"></label>',
188
+ );
189
+ });
190
+
191
+ it("supports data-* attributes", () => {
192
+ expect(<div data-id="42" data-name="test"></div>).toRender(
193
+ '<div data-id="42" data-name="test"></div>',
194
+ );
195
+ });
196
+
197
+ it("supports aria-* attributes", () => {
198
+ expect(<button aria-label="close" aria-expanded={false}></button>).toRender(
199
+ '<button aria-label="close"></button>',
200
+ );
201
+ });
202
+
203
+ it("supports numeric attribute values", () => {
204
+ expect(<input tabIndex={3} />).toRender('<input tabIndex="3" />');
205
+ });
206
+
207
+ it("strips function-valued attributes", () => {
208
+ expect(<div onFoo={() => {}} ref={() => {}}></div>).toRender("<div></div>");
209
+ });
210
+ });
211
+
212
+ describe("self-closing tags", () => {
213
+ it("renders br", () => {
214
+ expect(<br />).toRender("<br />");
215
+ });
216
+
217
+ it("renders hr", () => {
218
+ expect(<hr />).toRender("<hr />");
219
+ });
220
+
221
+ it("renders img with attributes", () => {
222
+ expect(<img src="cat.png" alt="A cat" />).toRender(
223
+ '<img src="cat.png" alt="A cat" />',
224
+ );
225
+ });
226
+
227
+ it("renders input with type and name", () => {
228
+ expect(<input type="text" name="q" />).toRender(
229
+ '<input type="text" name="q" />',
230
+ );
231
+ });
232
+
233
+ it("renders link tag", () => {
234
+ expect(<link rel="stylesheet" href="style.css" />).toRender(
235
+ '<link rel="stylesheet" href="style.css" />',
236
+ );
237
+ });
238
+
239
+ it("renders meta tag", () => {
240
+ expect(<meta name="description" content="hello" />).toRender(
241
+ '<meta name="description" content="hello" />',
242
+ );
243
+ });
244
+
245
+ it("renders source tag", () => {
246
+ expect(<source src="video.mp4" type="video/mp4" />).toRender(
247
+ '<source src="video.mp4" type="video/mp4" />',
248
+ );
249
+ });
66
250
  });
67
251
 
68
252
  describe("fragments", () => {
@@ -88,6 +272,169 @@ describe("fragments", () => {
88
272
  it("handles empty fragments", () => {
89
273
  expect(<></>).toRender("");
90
274
  });
275
+
276
+ it("encodes text inside a fragment", () => {
277
+ expect(<>{"<b>"}</>).toRender("&lt;b&gt;");
278
+ });
279
+ });
280
+
281
+ describe("script tag", () => {
282
+ it("renders script content without encoding", () => {
283
+ expect(<script>{"const x = 1 < 2 && 3 > 0;"}</script>).toRender(
284
+ "<script>const x = 1 < 2 && 3 > 0;</script>",
285
+ );
286
+ });
287
+
288
+ it("renders script with src attribute and no children", () => {
289
+ expect(<script src="app.js"></script>).toRender(
290
+ '<script src="app.js"></script>',
291
+ );
292
+ });
293
+
294
+ it("renders empty script tag", () => {
295
+ expect(<script></script>).toRender("<script></script>");
296
+ });
297
+ });
298
+
299
+ describe("style tag", () => {
300
+ it("renders style content", () => {
301
+ expect(<style>{"body { color: red; }"}</style>).toRender(
302
+ "<style>body{color:red}</style>",
303
+ );
304
+ });
305
+
306
+ it("minifies whitespace in style content", () => {
307
+ expect(
308
+ <style>{"body {\n color: red;\n margin: 0;\n}"}</style>,
309
+ ).toRender("<style>body{color:red;margin:0}</style>");
310
+ });
311
+
312
+ it("strips CSS comments", () => {
313
+ expect(
314
+ <style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
315
+ ).toRender("<style>body{margin:0}</style>");
316
+ });
317
+
318
+ it("preserves child combinator in selectors", () => {
319
+ expect(
320
+ <style>{":not(pre) > code { background: none; }"}</style>,
321
+ ).toRender("<style>:not(pre)>code{background:none}</style>");
322
+ });
323
+
324
+ it("preserves sibling combinators in selectors", () => {
325
+ expect(
326
+ <style>{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}</style>,
327
+ ).toRender("<style>h2+p{margin:0}h2~p{color:red}</style>");
328
+ });
329
+
330
+ it("removes spaces around braces, colons, and semicolons", () => {
331
+ expect(
332
+ <style>{"a { color : red ; font-size : 1em ; }"}</style>,
333
+ ).toRender("<style>a{color:red;font-size:1em}</style>");
334
+ });
335
+
336
+ it("removes trailing semicolon before closing brace", () => {
337
+ expect(
338
+ <style>{"p { margin: 0; padding: 0; }"}</style>,
339
+ ).toRender("<style>p{margin:0;padding:0}</style>");
340
+ });
341
+
342
+ it("does not break </style> injection", () => {
343
+ expect(
344
+ <style>{"a { content: '</style>'; }"}</style>,
345
+ ).toRender("<style>a{content:'<\\/style>'}</style>");
346
+ });
347
+ });
348
+
349
+ describe("dangerouslySetInnerHTML", () => {
350
+ it("renders raw HTML without encoding", () => {
351
+ expect(
352
+ <div dangerouslySetInnerHTML={{ __html: "<b>bold</b>" }}></div>,
353
+ ).toRender("<div><b>bold</b></div>");
354
+ });
355
+
356
+ it("does not double-encode HTML entities", () => {
357
+ expect(
358
+ <div dangerouslySetInnerHTML={{ __html: "&amp; &lt;" }}></div>,
359
+ ).toRender("<div>&amp; &lt;</div>");
360
+ });
361
+
362
+ it("preserves script tags in raw HTML", () => {
363
+ expect(
364
+ <div
365
+ dangerouslySetInnerHTML={{ __html: "<script>alert(1)</script>" }}
366
+ ></div>,
367
+ ).toRender("<div><script>alert(1)</script></div>");
368
+ });
369
+
370
+ it("does not render dangerouslySetInnerHTML as an attribute", () => {
371
+ expect(
372
+ <div dangerouslySetInnerHTML={{ __html: "hi" }}></div>,
373
+ ).toRender("<div>hi</div>");
374
+ });
375
+ });
376
+
377
+ describe("function components", () => {
378
+ it("renders a simple function component", () => {
379
+ const Greeting = ({ name }) => <p>Hello, {name}!</p>;
380
+ expect(<Greeting name="world" />).toRender("<p>Hello, world!</p>");
381
+ });
382
+
383
+ it("renders a component with children", () => {
384
+ const Box = ({ children }) => <div className="box">{children}</div>;
385
+ expect(
386
+ <Box>
387
+ <span>inside</span>
388
+ </Box>,
389
+ ).toRender('<div class="box"><span>inside</span></div>');
390
+ });
391
+
392
+ it("renders nested components", () => {
393
+ const Inner = () => <em>inner</em>;
394
+ const Outer = () => (
395
+ <div>
396
+ <Inner />
397
+ </div>
398
+ );
399
+ expect(<Outer />).toRender("<div><em>inner</em></div>");
400
+ });
401
+
402
+ it("renders a component returning null", () => {
403
+ const Empty = () => null;
404
+ expect(<Empty />).toRender("");
405
+ });
406
+
407
+ it("renders a component returning a string", () => {
408
+ const Text = () => "hello";
409
+ expect(<Text />).toRender("hello");
410
+ });
411
+
412
+ it("encodes text returned by a component", () => {
413
+ const Unsafe = () => "<script>alert(1)</script>";
414
+ expect(<Unsafe />).toRender(
415
+ "&lt;script&gt;alert(1)&lt;/script&gt;",
416
+ );
417
+ });
418
+ });
419
+
420
+ describe("forwardRef-like components", () => {
421
+ it("renders an object with a render function", () => {
422
+ const Button = { render: ({ children }) => <button>{children}</button> };
423
+ expect(<Button>click me</Button>).toRender(
424
+ "<button>click me</button>",
425
+ );
426
+ });
427
+
428
+ it("passes props to render function", () => {
429
+ const Input = {
430
+ render: ({ type, placeholder }) => (
431
+ <input type={type} placeholder={placeholder} />
432
+ ),
433
+ };
434
+ expect(<Input type="email" placeholder="you@example.com" />).toRender(
435
+ '<input type="email" placeholder="you@example.com" />',
436
+ );
437
+ });
91
438
  });
92
439
 
93
440
  describe("React element interop", () => {