@server/next 0.28.4 → 0.28.6

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.4",
3
+ "version": "0.28.6",
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(
@@ -17,14 +17,15 @@ const SELFCLOSE = new Set(
17
17
  );
18
18
 
19
19
  const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
20
- const REACT_ELEMENT_TYPE = Symbol.for("react.element");
21
20
 
22
21
  const altAttrs = {
23
22
  classname: "class",
23
+ htmlfor: "for",
24
24
  };
25
25
 
26
- // valid primitives only
27
- const isValidChild = (child) => child === 0 || child === "" || !!child;
26
+ // valid primitives only — null, undefined, false, true are all skipped
27
+ const isValidChild = (child) =>
28
+ child != null && child !== false && child !== true;
28
29
 
29
30
  // CSS helpers
30
31
  const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
@@ -32,28 +33,39 @@ const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
32
33
  const minifyCss = (str) =>
33
34
  str
34
35
  .replace(/\s+/g, " ")
35
- .replace(/\/\*[^*]*\*\//g, "")
36
+ .replace(/\/\*[\s\S]*?\*\//g, "")
36
37
  .trim();
37
38
 
38
- // React element detection (safe for your custom objects too)
39
+ // React element detection (safe for custom objects too)
39
40
  const isReactElement = (val) =>
40
41
  val !== null && typeof val === "object" && "type" in val && "props" in val;
41
42
 
42
- // 🔥 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
+
43
47
  const renderChild = (child) => {
44
- if (child == null || child === false) return "";
48
+ if (child == null || child === false || child === true) return "";
45
49
 
46
50
  if (Array.isArray(child)) {
47
51
  return child.map(renderChild).join("");
48
52
  }
49
53
 
50
54
  if (typeof child === "function") {
51
- 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);
52
61
  }
53
62
 
54
- if (typeof child === "string") return child;
63
+ if (typeof child === "string") return encode(child);
55
64
  if (typeof child === "number") return String(child);
56
65
 
66
+ // Raw marker bypasses encoding (used by script, style, dangerouslySetInnerHTML)
67
+ if (typeof child === "object" && RAW in child) return child[RAW];
68
+
57
69
  if (isReactElement(child)) {
58
70
  return jsx(child.type, child.props || {})();
59
71
  }
@@ -63,7 +75,7 @@ const renderChild = (child) => {
63
75
  };
64
76
 
65
77
  const jsx = (tag, { children, ...props } = {}) => {
66
- // 🔥 Fragment (Symbol-safe)
78
+ // Fragment (Symbol-safe)
67
79
  if (tag === REACT_FRAGMENT_TYPE || tag === Fragment) {
68
80
  return () => renderChild(children);
69
81
  }
@@ -82,21 +94,24 @@ const jsx = (tag, { children, ...props } = {}) => {
82
94
  return () => renderChild(tag.render({ children, ...props }, null));
83
95
  }
84
96
 
85
- // script special-case
86
- if (tag === "script" && children) {
87
- const src = children;
88
- 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);
89
105
  }
90
106
 
91
- // style special-case
107
+ // style: minified + escaped raw content
92
108
  if (tag === "style" && typeof children === "string") {
93
- const src = minifyCss(escapeCSS(children));
94
- children = () => src;
109
+ children = raw(minifyCss(escapeCSS(children)));
95
110
  }
96
111
 
97
112
  // dangerouslySetInnerHTML
98
113
  if (props?.dangerouslySetInnerHTML) {
99
- children = () => props.dangerouslySetInnerHTML.__html;
114
+ children = raw(props.dangerouslySetInnerHTML.__html ?? "");
100
115
  }
101
116
 
102
117
  // normalize children
@@ -105,7 +120,7 @@ const jsx = (tag, { children, ...props } = {}) => {
105
120
  .map(renderChild)
106
121
  .join("");
107
122
 
108
- // 🔥 CRITICAL SAFETY GATE (prevents Symbol/string crash)
123
+ // safety gate (prevents Symbol/unknown-type crash)
109
124
  if (!tag || typeof tag !== "string") {
110
125
  return () => flatChildren;
111
126
  }
@@ -121,7 +136,7 @@ const jsx = (tag, { children, ...props } = {}) => {
121
136
  if (v === true) return key;
122
137
 
123
138
  const value =
124
- typeof v === "string" || typeof v === "number" ? encode(v) : "";
139
+ typeof v === "string" || typeof v === "number" ? encode(String(v)) : "";
125
140
 
126
141
  return `${key}="${value}"`;
127
142
  })
@@ -138,6 +153,6 @@ const jsx = (tag, { children, ...props } = {}) => {
138
153
  return () => `${doctype}<${tag}${attrStr}>${flatChildren}</${tag}>`;
139
154
  };
140
155
 
141
- const Fragment = "";
156
+ const Fragment = Symbol.for("react.fragment");
142
157
 
143
158
  export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment };
@@ -44,6 +44,209 @@ describe("jsx", () => {
44
44
  `<!DOCTYPE html><html lang="en">Hello</html>`,
45
45
  );
46
46
  });
47
+
48
+ it("can render a pre with text", () => {
49
+ expect(<pre>hello</pre>).toRender("<pre>hello</pre>");
50
+ });
51
+
52
+ it("can render a pre with newlines", () => {
53
+ expect(<pre>line1{"\n"}line2</pre>).toRender("<pre>line1\nline2</pre>");
54
+ });
55
+
56
+ it("can render a pre with indented text", () => {
57
+ expect(<pre>{` indented`}</pre>).toRender("<pre> indented</pre>");
58
+ });
59
+
60
+ it("can render a pre inside a div", () => {
61
+ expect(
62
+ <div>
63
+ <pre>line1{"\n"}line2</pre>
64
+ </div>,
65
+ ).toRender("<div><pre>line1\nline2</pre></div>");
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
+ });
47
250
  });
48
251
 
49
252
  describe("fragments", () => {
@@ -69,6 +272,139 @@ describe("fragments", () => {
69
272
  it("handles empty fragments", () => {
70
273
  expect(<></>).toRender("");
71
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
+
319
+ describe("dangerouslySetInnerHTML", () => {
320
+ it("renders raw HTML without encoding", () => {
321
+ expect(
322
+ <div dangerouslySetInnerHTML={{ __html: "<b>bold</b>" }}></div>,
323
+ ).toRender("<div><b>bold</b></div>");
324
+ });
325
+
326
+ it("does not double-encode HTML entities", () => {
327
+ expect(
328
+ <div dangerouslySetInnerHTML={{ __html: "&amp; &lt;" }}></div>,
329
+ ).toRender("<div>&amp; &lt;</div>");
330
+ });
331
+
332
+ it("preserves script tags in raw HTML", () => {
333
+ expect(
334
+ <div
335
+ dangerouslySetInnerHTML={{ __html: "<script>alert(1)</script>" }}
336
+ ></div>,
337
+ ).toRender("<div><script>alert(1)</script></div>");
338
+ });
339
+
340
+ it("does not render dangerouslySetInnerHTML as an attribute", () => {
341
+ expect(
342
+ <div dangerouslySetInnerHTML={{ __html: "hi" }}></div>,
343
+ ).toRender("<div>hi</div>");
344
+ });
345
+ });
346
+
347
+ describe("function components", () => {
348
+ it("renders a simple function component", () => {
349
+ const Greeting = ({ name }) => <p>Hello, {name}!</p>;
350
+ expect(<Greeting name="world" />).toRender("<p>Hello, world!</p>");
351
+ });
352
+
353
+ it("renders a component with children", () => {
354
+ const Box = ({ children }) => <div className="box">{children}</div>;
355
+ expect(
356
+ <Box>
357
+ <span>inside</span>
358
+ </Box>,
359
+ ).toRender('<div class="box"><span>inside</span></div>');
360
+ });
361
+
362
+ it("renders nested components", () => {
363
+ const Inner = () => <em>inner</em>;
364
+ const Outer = () => (
365
+ <div>
366
+ <Inner />
367
+ </div>
368
+ );
369
+ expect(<Outer />).toRender("<div><em>inner</em></div>");
370
+ });
371
+
372
+ it("renders a component returning null", () => {
373
+ const Empty = () => null;
374
+ expect(<Empty />).toRender("");
375
+ });
376
+
377
+ it("renders a component returning a string", () => {
378
+ const Text = () => "hello";
379
+ expect(<Text />).toRender("hello");
380
+ });
381
+
382
+ it("encodes text returned by a component", () => {
383
+ const Unsafe = () => "<script>alert(1)</script>";
384
+ expect(<Unsafe />).toRender(
385
+ "&lt;script&gt;alert(1)&lt;/script&gt;",
386
+ );
387
+ });
388
+ });
389
+
390
+ describe("forwardRef-like components", () => {
391
+ it("renders an object with a render function", () => {
392
+ const Button = { render: ({ children }) => <button>{children}</button> };
393
+ expect(<Button>click me</Button>).toRender(
394
+ "<button>click me</button>",
395
+ );
396
+ });
397
+
398
+ it("passes props to render function", () => {
399
+ const Input = {
400
+ render: ({ type, placeholder }) => (
401
+ <input type={type} placeholder={placeholder} />
402
+ ),
403
+ };
404
+ expect(<Input type="email" placeholder="you@example.com" />).toRender(
405
+ '<input type="email" placeholder="you@example.com" />',
406
+ );
407
+ });
72
408
  });
73
409
 
74
410
  describe("React element interop", () => {