@server/next 0.28.8 → 0.28.10

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.8",
3
+ "version": "0.28.10",
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",
@@ -47,6 +47,7 @@
47
47
  "@types/bun": "^1.3.0",
48
48
  "@types/jest": "^30.0.0",
49
49
  "@types/node": "^24.10.0",
50
+ "bun": "^1.3.13",
50
51
  "check-dts": "^0.8.2",
51
52
  "jest": "^29.7.0",
52
53
  "polystore": "^0.18.0",
package/readme.md CHANGED
@@ -1 +1 @@
1
- # Server JS
1
+ # Server JS [![test badge](https://github.com/franciscop/server-next/workflows/tests/badge.svg)](https://github.com/franciscop/server-next/actions)
@@ -1,4 +1,4 @@
1
- const entities = {
1
+ const ENTITIES = {
2
2
  "&": "&",
3
3
  "<": "&lt;",
4
4
  ">": "&gt;",
@@ -6,23 +6,34 @@ const entities = {
6
6
  "'": "&#39;",
7
7
  };
8
8
 
9
- const encode = (str = "") => {
10
- if (typeof str === "number") str = String(str);
11
- if (typeof str !== "string") return "";
12
- return str.replace(/[&<>"']/g, (tag) => entities[tag]);
13
- };
14
-
15
9
  const SELFCLOSE = new Set(
16
10
  "area,base,br,col,embed,hr,img,input,link,meta,source,track,wbr".split(","),
17
11
  );
18
12
 
13
+ const SVG_TAGS = new Set([
14
+ "svg",
15
+ "circle",
16
+ "rect",
17
+ "path",
18
+ "line",
19
+ "g",
20
+ "text",
21
+ "defs",
22
+ ]);
23
+
19
24
  const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
20
25
 
21
- const altAttrs = {
26
+ const ALT_NAMES = {
22
27
  classname: "class",
23
28
  htmlfor: "for",
24
29
  };
25
30
 
31
+ const encode = (str = "") => {
32
+ if (typeof str === "number") str = String(str);
33
+ if (typeof str !== "string") return "";
34
+ return str.replace(/[&<>"']/g, (tag) => ENTITIES[tag]);
35
+ };
36
+
26
37
  // valid primitives only — null, undefined, false, true are all skipped
27
38
  const isValidChild = (child) =>
28
39
  child != null && child !== false && child !== true;
@@ -53,7 +64,6 @@ const renderChild = (child) => {
53
64
 
54
65
  if (typeof child === "function") {
55
66
  const result = child();
56
- // Strings returned by functions are pre-rendered HTML — don't re-encode them
57
67
  if (result == null || result === false || result === true) return "";
58
68
  if (typeof result === "string") return result;
59
69
  if (typeof result === "number") return String(result);
@@ -63,7 +73,6 @@ const renderChild = (child) => {
63
73
  if (typeof child === "string") return encode(child);
64
74
  if (typeof child === "number") return String(child);
65
75
 
66
- // Raw marker bypasses encoding (used by script, style, dangerouslySetInnerHTML)
67
76
  if (typeof child === "object" && RAW in child) return child[RAW];
68
77
 
69
78
  if (isReactElement(child)) {
@@ -99,7 +108,9 @@ const jsx = (tag, { children, ...props } = {}) => {
99
108
  const parts = Array.isArray(children) ? children : [children];
100
109
  const content = parts
101
110
  .filter(isValidChild)
102
- .map((c) => (typeof c === "string" ? c : typeof c === "number" ? String(c) : ""))
111
+ .map((c) =>
112
+ typeof c === "string" ? c : typeof c === "number" ? String(c) : "",
113
+ )
103
114
  .join("");
104
115
  children = raw(content);
105
116
  }
@@ -131,10 +142,28 @@ const jsx = (tag, { children, ...props } = {}) => {
131
142
  .filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
132
143
  .filter(([, v]) => v !== false)
133
144
  .map(([k, v]) => {
134
- const key = altAttrs[k.toLowerCase()] || encode(k);
145
+ const preserveSvg = k === "viewBox" || k === "preserveAspectRatio";
146
+
147
+ const key =
148
+ ALT_NAMES[k.toLowerCase()] ||
149
+ (SVG_TAGS.has(tag)
150
+ ? preserveSvg
151
+ ? k
152
+ : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
153
+ : encode(k));
135
154
 
136
155
  if (v === true) return key;
137
156
 
157
+ if (k === "style" && v && typeof v === "object") {
158
+ v = Object.entries(v)
159
+ .filter(([, val]) => val != null)
160
+ .map(([prop, val]) => {
161
+ const cssKey = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
162
+ return `${cssKey}:${val}`;
163
+ })
164
+ .join(";");
165
+ }
166
+
138
167
  const value =
139
168
  typeof v === "string" || typeof v === "number" ? encode(String(v)) : "";
140
169
 
@@ -142,9 +142,13 @@ describe("text encoding", () => {
142
142
 
143
143
  it("renders array children", () => {
144
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
- );
145
+ expect(
146
+ <span>
147
+ {items.map((i) => (
148
+ <b key={i}>{i}</b>
149
+ ))}
150
+ </span>,
151
+ ).toRender("<span><b>x</b><b>y</b><b>z</b></span>");
148
152
  });
149
153
 
150
154
  it("treats string returned from a function child as raw HTML", () => {
@@ -171,9 +175,7 @@ describe("attribute encoding", () => {
171
175
  });
172
176
 
173
177
  it("encodes ' in attribute values", () => {
174
- expect(<div title={"it's"}></div>).toRender(
175
- '<div title="it&#39;s"></div>',
176
- );
178
+ expect(<div title={"it's"}></div>).toRender('<div title="it&#39;s"></div>');
177
179
  });
178
180
 
179
181
  it("maps className to class", () => {
@@ -316,9 +318,9 @@ describe("style tag", () => {
316
318
  });
317
319
 
318
320
  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>");
321
+ expect(<style>{":not(pre) > code { background: none; }"}</style>).toRender(
322
+ "<style>:not(pre)>code{background:none}</style>",
323
+ );
322
324
  });
323
325
 
324
326
  it("preserves sibling combinators in selectors", () => {
@@ -328,21 +330,21 @@ describe("style tag", () => {
328
330
  });
329
331
 
330
332
  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>");
333
+ expect(<style>{"a { color : red ; font-size : 1em ; }"}</style>).toRender(
334
+ "<style>a{color:red;font-size:1em}</style>",
335
+ );
334
336
  });
335
337
 
336
338
  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>");
339
+ expect(<style>{"p { margin: 0; padding: 0; }"}</style>).toRender(
340
+ "<style>p{margin:0;padding:0}</style>",
341
+ );
340
342
  });
341
343
 
342
344
  it("does not break </style> injection", () => {
343
- expect(
344
- <style>{"a { content: '</style>'; }"}</style>,
345
- ).toRender("<style>a{content:'<\\/style>'}</style>");
345
+ expect(<style>{"a { content: '</style>'; }"}</style>).toRender(
346
+ "<style>a{content:'<\\/style>'}</style>",
347
+ );
346
348
  });
347
349
  });
348
350
 
@@ -368,9 +370,9 @@ describe("dangerouslySetInnerHTML", () => {
368
370
  });
369
371
 
370
372
  it("does not render dangerouslySetInnerHTML as an attribute", () => {
371
- expect(
372
- <div dangerouslySetInnerHTML={{ __html: "hi" }}></div>,
373
- ).toRender("<div>hi</div>");
373
+ expect(<div dangerouslySetInnerHTML={{ __html: "hi" }}></div>).toRender(
374
+ "<div>hi</div>",
375
+ );
374
376
  });
375
377
  });
376
378
 
@@ -411,18 +413,14 @@ describe("function components", () => {
411
413
 
412
414
  it("encodes text returned by a component", () => {
413
415
  const Unsafe = () => "<script>alert(1)</script>";
414
- expect(<Unsafe />).toRender(
415
- "&lt;script&gt;alert(1)&lt;/script&gt;",
416
- );
416
+ expect(<Unsafe />).toRender("&lt;script&gt;alert(1)&lt;/script&gt;");
417
417
  });
418
418
  });
419
419
 
420
420
  describe("forwardRef-like components", () => {
421
421
  it("renders an object with a render function", () => {
422
422
  const Button = { render: ({ children }) => <button>{children}</button> };
423
- expect(<Button>click me</Button>).toRender(
424
- "<button>click me</button>",
425
- );
423
+ expect(<Button>click me</Button>).toRender("<button>click me</button>");
426
424
  });
427
425
 
428
426
  it("passes props to render function", () => {
@@ -487,3 +485,147 @@ describe("React element interop", () => {
487
485
  expect(<Deep />).toRender("<div><ul><li>one</li><li>two</li></ul></div>");
488
486
  });
489
487
  });
488
+ describe("styles", () => {
489
+ it("stringifies style object into CSS", () => {
490
+ expect(
491
+ <div
492
+ style={{
493
+ display: "inline-block",
494
+ padding: "2px 8px",
495
+ borderRadius: "4px",
496
+ }}
497
+ />,
498
+ ).toRender(
499
+ '<div style="display:inline-block;padding:2px 8px;border-radius:4px"></div>',
500
+ );
501
+ });
502
+
503
+ it("handles numbers and mixed style values", () => {
504
+ expect(
505
+ <div
506
+ style={{
507
+ fontSize: 12,
508
+ lineHeight: 1.5,
509
+ fontWeight: "600",
510
+ }}
511
+ />,
512
+ ).toRender(
513
+ '<div style="font-size:12;line-height:1.5;font-weight:600"></div>',
514
+ );
515
+ });
516
+
517
+ it("converts camelCase CSS properties", () => {
518
+ expect(
519
+ <div
520
+ style={{
521
+ backgroundColor: "#fff",
522
+ marginTop: "10px",
523
+ }}
524
+ />,
525
+ ).toRender('<div style="background-color:#fff;margin-top:10px"></div>');
526
+ });
527
+
528
+ it("ignores null and undefined style values", () => {
529
+ expect(
530
+ <div
531
+ style={{
532
+ color: "red",
533
+ padding: null,
534
+ margin: undefined,
535
+ }}
536
+ />,
537
+ ).toRender('<div style="color:red"></div>');
538
+ });
539
+ });
540
+
541
+ describe("svg attributes", () => {
542
+ it("converts strokeWidth to stroke-width", () => {
543
+ expect(
544
+ <svg>
545
+ <circle strokeWidth="2" />
546
+ </svg>,
547
+ ).toRender('<svg><circle stroke-width="2"></circle></svg>');
548
+ });
549
+
550
+ it("converts fillOpacity to fill-opacity", () => {
551
+ expect(
552
+ <svg>
553
+ <rect fillOpacity="0.5" />
554
+ </svg>,
555
+ ).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
556
+ });
557
+
558
+ it("converts strokeLinecap to stroke-linecap", () => {
559
+ expect(
560
+ <svg>
561
+ <line strokeLinecap="round" />
562
+ </svg>,
563
+ ).toRender('<svg><line stroke-linecap="round"></line></svg>');
564
+ });
565
+
566
+ it("does NOT break viewBox casing", () => {
567
+ expect(
568
+ <svg viewBox="0 0 100 100">
569
+ <rect />
570
+ </svg>,
571
+ ).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
572
+ });
573
+
574
+ it("handles mixed SVG attributes correctly", () => {
575
+ expect(
576
+ <svg viewBox="0 0 10 10">
577
+ <circle strokeWidth="1" fillOpacity="0.2" />
578
+ </svg>,
579
+ ).toRender(
580
+ '<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
581
+ );
582
+ });
583
+
584
+ it("keeps viewBox unchanged", () => {
585
+ expect(
586
+ <svg viewBox="0 0 100 100">
587
+ <rect />
588
+ </svg>,
589
+ ).toRender('<svg viewBox="0 0 100 100"><rect></rect></svg>');
590
+ });
591
+
592
+ it("does NOT convert viewBox to view-box", () => {
593
+ expect(<svg viewBox="0 0 10 10"></svg>).toRender(
594
+ '<svg viewBox="0 0 10 10"></svg>',
595
+ );
596
+ });
597
+
598
+ it("converts strokeWidth to stroke-width", () => {
599
+ expect(
600
+ <svg>
601
+ <circle strokeWidth="2" />
602
+ </svg>,
603
+ ).toRender('<svg><circle stroke-width="2"></circle></svg>');
604
+ });
605
+
606
+ it("converts fillOpacity to fill-opacity", () => {
607
+ expect(
608
+ <svg>
609
+ <rect fillOpacity="0.5" />
610
+ </svg>,
611
+ ).toRender('<svg><rect fill-opacity="0.5"></rect></svg>');
612
+ });
613
+
614
+ it("converts strokeLinecap to stroke-linecap", () => {
615
+ expect(
616
+ <svg>
617
+ <line strokeLinecap="round" />
618
+ </svg>,
619
+ ).toRender('<svg><line stroke-linecap="round"></line></svg>');
620
+ });
621
+
622
+ it("handles mixed SVG attributes correctly", () => {
623
+ expect(
624
+ <svg viewBox="0 0 10 10">
625
+ <circle strokeWidth="1" fillOpacity="0.2" />
626
+ </svg>,
627
+ ).toRender(
628
+ '<svg viewBox="0 0 10 10"><circle stroke-width="1" fill-opacity="0.2"></circle></svg>',
629
+ );
630
+ });
631
+ });